<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>全屏画板 · 粘贴/上传图片 · 拖动缩放 · 导出</title>
    <style>
      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
        font-family: 'SF Pro Display', 'PingFang SC', 'Helvetica Neue', Arial,
          sans-serif;
      }

      :root {
        --primary-bg: #e6e7ee;
        --secondary-bg: #ffffff;
        --shadow-light: #ffffff;
        --shadow-dark: #d1d9e6;
        --accent-color: #667eea;
        --accent-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        /* 半透明主色使用的 RGB（默认与 #667eea 对应的 102,126,234）*/
        --accent-rgb: 102, 126, 234;
        --success-color: #4ecdc4;
        --danger-color: #ff6b6b;
        --warning-color: #feca57;
        --text-primary: #2d3748;
        --text-secondary: #718096;
        --glass-bg: rgba(255, 255, 255, 0.25);
        --glass-border: rgba(255, 255, 255, 0.18);
      }

      body {
        background: var(--primary-bg);
        color: var(--text-primary);
        min-height: 100vh;
        overflow: hidden;
      }

      /* 全屏画板区域 */
      .board {
        position: fixed;
        inset: 0;
        background: var(--primary-bg);
        overflow: hidden;
        transform-origin: center center;
        will-change: transform;
      }

      /* 画板拖拽模式光标 */
      .board.pan-mode {
        cursor: grab;
      }
      .board.pan-active {
        cursor: grabbing;
      }

      /* 画板内容容器：仅移动内容，不移动画板本身 */
      .board-content {
        position: absolute;
        inset: 0;
        will-change: transform;
        transform-origin: 0 0;
      }

      /* 顶部工具栏（参考 split 风格） */
      .toolbar {
        position: fixed;
        top: 20px;
        left: 50%;
        /* 为切入/退出动效改为同时设置 X/Y 位移 */
        transform: translate(-50%, 0);
        transition: transform 0.28s ease, opacity 0.28s ease;
        width: calc(100% - 80px);
        max-width: 1200px;
        background: var(--primary-bg);
        border-radius: 25px;
        padding: 16px 20px;
        display: flex;
        gap: 12px;
        align-items: center;
        box-shadow: 10px 10px 20px var(--shadow-dark),
          -10px -10px 20px var(--shadow-light);
        z-index: 1000;
      }
      /* 工具栏隐藏状态：向上退出 */
      .toolbar.hidden {
        transform: translate(-50%, -120%);
        opacity: 0;
        pointer-events: none;
      }

      .toolbar .title {
        font-size: 1.3rem;
        font-weight: 700;
        background: var(--accent-gradient);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        background-clip: text;
        margin-right: auto;
      }

      .meta {
        color: var(--text-secondary);
        font-size: 0.9rem;
        background: rgba(102, 126, 234, 0.08);
        padding: 8px 12px;
        border-radius: 18px;
        border: 1px solid rgba(102, 126, 234, 0.2);
      }

      button {
        background: var(--primary-bg);
        color: var(--text-primary);
        border: none;
        padding: 12px 18px;
        border-radius: 15px;
        cursor: pointer;
        font-weight: 600;
        transition: all 0.3s ease;
        font-size: 0.95rem;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
        position: relative;
        overflow: hidden;
      }

      button::before {
        content: '';
        position: absolute;
        top: 0;
        left: -100%;
        width: 100%;
        height: 100%;
        background: linear-gradient(
          90deg,
          transparent,
          rgba(255, 255, 255, 0.2),
          transparent
        );
        transition: left 0.5s;
      }

      button:hover::before {
        left: 100%;
      }
      button:hover {
        transform: translateY(-2px);
      }
      button:active {
        transform: translateY(0);
      }

      /* 小图标按钮（用于帮助/快捷键入口） */
      .icon-btn {
        padding: 8px 10px;
        border-radius: 12px;
        font-size: 0.95rem;
        line-height: 1;
      }

      .count-with-help {
        display: flex;
        align-items: center;
        gap: 8px;
      }

      /* 快捷键设置弹框 */
      .shortcut-modal {
        position: fixed;
        inset: 0;
        background: rgba(0, 0, 0, 0.45);
        display: none;
        align-items: center;
        justify-content: center;
        z-index: 2000;
      }
      .shortcut-modal .modal-content {
        background: var(--primary-bg);
        color: var(--text-primary);
        width: 520px;
        max-width: calc(100% - 40px);
        border-radius: 16px;
        padding: 16px 16px 12px;
      }
      .shortcut-modal .modal-header {
        display: flex;
        align-items: center;
        justify-content: space-between;
        margin-bottom: 10px;
      }
      .shortcut-modal h3 {
        margin: 0;
        font-size: 1.1rem;
        background: var(--accent-gradient);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        background-clip: text;
      }
      .shortcut-list {
        display: grid;
        grid-template-columns: 110px 1fr auto auto;
        gap: 8px 10px;
        align-items: center;
      }
      .shortcut-list .label {
        color: var(--text-secondary);
        font-size: 0.95rem;
      }
      .shortcut-input {
        width: 100%;
        padding: 8px 10px;
        border-radius: 10px;
        border: 1px solid rgba(102, 126, 234, 0.35);
        background: var(--primary-bg);
        color: var(--text-primary);
      }
      .modal-actions {
        display: flex;
        align-items: center;
        justify-content: flex-end;
        gap: 8px;
        margin-top: 12px;
      }
      .shortcut-tip {
        margin-top: 8px;
        font-size: 0.85rem;
        color: var(--text-secondary);
      }

      .btn-primary {
        background: var(--accent-gradient);
        color: white;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light),
          inset 0 1px 0 rgba(255, 255, 255, 0.2);
      }

      .btn-danger {
        background: linear-gradient(
          135deg,
          var(--danger-color) 0%,
          #e65a5a 100%
        );
        color: white;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light),
          inset 0 1px 0 rgba(255, 255, 255, 0.2);
      }

      /* 画板中的图片项 */
      .item {
        position: absolute;
        border-radius: 16px;
        user-select: none;
      }

      /* 形状与文本项样式 */
      .item.shape {
        background: transparent;
      }
      .item.shape.rect {
        border: 3px dashed rgba(var(--accent-rgb, 102, 126, 234), 0.8);
      }
      .item.shape.circle {
        border: 3px dashed rgba(var(--accent-rgb, 102, 126, 234), 0.8);
        border-radius: 50%;
      }
      .item.text-item {
        padding: 6px 10px;
        border-radius: 10px;
        color: var(--text-primary);
      }
      .item.text-item .text-content {
        outline: none;
        user-select: text;
        cursor: text;
        font-size: 16px;
        min-width: 40px;
        min-height: 24px;
      }
      .item.doodle svg {
        width: 100%;
        height: 100%;
        display: block;
      }
      .item.arrow svg {
        width: 100%;
        height: 100%;
        display: block;
      }

      .item.selected {
        outline: 2px dashed rgba(var(--accent-rgb), 0.6);
        outline-offset: 2px;
      }

      /* 框选矩形（马赛克式半透明填充 + 虚线边框） */
      .marquee {
        position: absolute;
        border: 2px dashed rgba(var(--accent-rgb), 0.85);
        background: rgba(var(--accent-rgb), 0.12);
        border-radius: 8px;
        pointer-events: none;
        z-index: 9999;
      }

      /* 对齐辅助线 */
      .align-guide {
        position: absolute;
        pointer-events: none;
        background: rgba(var(--accent-rgb), 0.9);
        box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.8);
        z-index: 10000;
      }
      .align-guide.x {
        width: 2px;
        top: 0;
        height: 100%;
      }
      .align-guide.y {
        height: 2px;
        left: 0;
        width: 100%;
      }

      /* 距离提示气泡 */
      .distance-indicator {
        position: absolute;
        pointer-events: none;
        display: none;
        padding: 6px 10px;
        border-radius: 10px;
        background: rgba(0, 0, 0, 0.7);
        color: #fff;
        font-size: 12px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
        z-index: 10001;
        transform: translate(-50%, -100%);
        white-space: nowrap;
      }

      /* 距离线段（Alt 悬停显示的 Δx/Δy 以线段样式呈现） */
      .measure-line {
        position: absolute;
        pointer-events: none;
        background: rgba(var(--accent-rgb), 0.9);
        box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.8);
        z-index: 10001;
      }
      .measure-line.x {
        height: 2px;
      }
      .measure-line.y {
        width: 2px;
      }
      .measure-line .label {
        position: absolute;
        background: rgba(0, 0, 0, 0.6);
        color: #fff;
        font-size: 12px;
        line-height: 1;
        padding: 2px 6px;
        border-radius: 6px;
        box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
        pointer-events: none;
        white-space: nowrap;
      }

      .item img {
        width: 100%;
        height: 100%;
        display: block;
        pointer-events: none;
      }

      /* 四角缩放手柄 */
      .handle {
        position: absolute;
        width: 12px;
        height: 12px;
        border-radius: 50%;
        background: var(--accent-color);
        border: 2px solid #fff;
        box-shadow: 0 2px 6px rgba(var(--accent-rgb), 0.4);
        cursor: nwse-resize;
      }
      /* 默认隐藏缩放手柄，仅选中时展示 */
      .item .handle {
        display: none;
      }
      .item.selected .handle {
        display: block;
      }

      .handle.nw {
        left: -6px;
        top: -6px;
        cursor: nwse-resize;
      }
      .handle.ne {
        right: -6px;
        top: -6px;
        cursor: nesw-resize;
      }
      .handle.sw {
        left: -6px;
        bottom: -6px;
        cursor: nesw-resize;
      }
      .handle.se {
        right: -6px;
        bottom: -6px;
        cursor: nwse-resize;
      }

      /* 底部状态栏 */
      .statusbar {
        position: fixed;
        bottom: 20px;
        left: 50%;
        transform: translate(-50%, 0);
        transition: transform 0.28s ease, opacity 0.28s ease;
        width: calc(100% - 80px);
        max-width: 1200px;
        background: var(--primary-bg);
        border-radius: 20px;
        padding: 12px 16px;
        color: var(--text-secondary);
        display: flex;
        justify-content: space-between;
        align-items: center;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
        z-index: 1000;
      }
      /* 状态栏隐藏状态：向下退出 */
      .statusbar.hidden {
        transform: translate(-50%, 120%);
        opacity: 0;
        pointer-events: none;
      }

      /* 右下角 UI 显隐切换按钮 */
      .ui-toggle {
        position: fixed;
        right: 20px;
        bottom: 20px;
        z-index: 1002;
        background: var(--primary-bg);
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
      }
      .status-right {
        display: flex;
        align-items: center;
        gap: 12px;
      }
      /* 已移除缩放指示器样式 */

      /* 画板尺寸输入样式 */
      .board-size {
        display: flex;
        align-items: center;
        gap: 6px;
        background: rgba(var(--accent-rgb), 0.08);
        padding: 6px 10px;
        border-radius: 12px;
        border: 1px solid rgba(var(--accent-rgb), 0.2);
        color: var(--text-secondary);
        font-size: 0.9rem;
      }
      .board-size label {
        color: var(--text-secondary);
        font-size: 0.85rem;
      }
      .size-input {
        width: 96px;
        padding: 6px 8px;
        border-radius: 8px;
        border: 1px solid rgba(var(--accent-rgb), 0.2);
        background: var(--secondary-bg);
        color: var(--text-primary);
        box-shadow: inset 2px 2px 6px var(--shadow-dark),
          inset -2px -2px 6px var(--shadow-light);
      }

      .hidden-input {
        position: absolute;
        width: 1px;
        height: 1px;
        opacity: 0;
        pointer-events: none;
      }

      /* 右键菜单 */
      .context-menu {
        position: fixed;
        z-index: 1001;
        display: none;
        min-width: 160px;
        background: var(--primary-bg);
        border-radius: 12px;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
        padding: 8px;
        border: 1px solid rgba(var(--accent-rgb), 0.2);
      }
      .context-menu button {
        width: 100%;
        text-align: left;
        padding: 10px 12px;
        border-radius: 8px;
        background: var(--primary-bg);
        font-size: 0.95rem;
        margin-bottom: 5px;
      }
      .context-menu button:hover {
        background: rgba(var(--accent-rgb), 0.12);
      }

      /* 导出模式下拉框美化 */
      .select-wrap {
        position: relative;
      }
      .toolbar select {
        appearance: none;
        -webkit-appearance: none;
        background: var(--primary-bg);
        color: var(--text-primary);
        border: none;
        padding: 12px 38px 12px 16px;
        border-radius: 15px;
        cursor: pointer;
        font-weight: 600;
        transition: all 0.3s ease;
        font-size: 0.95rem;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
      }
      .toolbar select:hover {
        transform: translateY(-2px);
      }
      .toolbar select:active {
        transform: translateY(0);
      }
      .select-wrap::after {
        content: '\25BC';
        position: absolute;
        right: 12px;
        top: 50%;
        transform: translateY(-50%);
        color: var(--text-secondary);
        pointer-events: none;
        font-size: 0.85rem;
        opacity: 0.8;
      }
      /* 全局颜色选择器样式 */
      .color-wrap {
        position: relative;
        display: inline-flex;
        align-items: center;
        gap: 8px;
        background: var(--primary-bg);
        color: var(--text-primary);
        padding: 8px 12px;
        border-radius: 15px;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
        /* 按需求隐藏全局颜色配置项展示，仅保留内部变量逻辑 */
        display: none;
      }
      .color-wrap label {
        font-size: 0.9rem;
        color: var(--text-secondary);
      }
      .color-wrap input[type='color'] {
        width: 36px;
        height: 24px;
        border: none;
        padding: 0;
        border-radius: 6px;
        background: transparent;
        cursor: pointer;
        box-shadow: inset 4px 4px 8px var(--shadow-dark),
          inset -4px -4px 8px var(--shadow-light);
      }
    </style>
  </head>
  <body>
    <div id="board" class="board" tabindex="0" aria-label="全屏画板">
      <div id="boardContent" class="board-content"></div>
    </div>

    <div class="toolbar">
      <div class="title">全屏画板</div>
      <div class="meta" id="launchDate">上线日期加载中…</div>
      <div class="color-wrap" title="全局颜色">
        <label for="globalColor">颜色</label>
        <input type="color" id="globalColor" value="#667eea" />
      </div>
      <div class="select-wrap" title="导出尺寸模式">
        <select id="exportMode">
          <option value="largest" selected>最大图片尺寸</option>
          <option value="content">内容包裹</option>
          <option value="board">画板尺寸</option>
        </select>
      </div>
      <button id="btnImport" class="btn-primary">导入图片</button>
      <button id="btnExport" class="btn-primary">导出 PNG</button>
      <button id="btnExportSelected" class="btn-primary">导出所选</button>
      <button id="btnClear" class="btn-danger">清空画板</button>
      <input
        id="fileInput"
        class="hidden-input"
        type="file"
        accept="image/*"
        multiple
      />
    </div>

    <!-- 文字配置面板（旧：不再展示，保留以兼容逻辑） -->
    <div
      id="textConfigPanel"
      class="text-config-panel"
      style="display: none"
    ></div>

    <!-- 文本内联操作栏：在选中文本元素下方展示字号与颜色控制 -->
    <div
      id="textInlineToolbar"
      class="text-inline-toolbar"
      style="display: none"
    >
      <div class="inline-inner">
        <div class="inline-item" title="字号">
          <span class="inline-label">字号</span>
          <input
            id="inlineFontSize"
            type="number"
            min="10"
            max="200"
            step="1"
            value="16"
          />
        </div>
        <div class="inline-item" title="颜色">
          <span class="inline-label">颜色</span>
          <input id="inlineColor" type="color" />
        </div>
      </div>
    </div>

    <div class="statusbar">
      <div id="status">提示：可 Ctrl+V 直接粘贴图片，或拖拽文件到画板。</div>
      <div class="status-right">
        <div id="zoomIndicator" class="zoom-indicator">缩放 100%</div>
        <button id="btnResetZoom" class="btn-primary" title="重置为100%">
          100%
        </button>
        <div class="board-size" title="画板宽高（用于画板尺寸导出）">
          <label for="boardWidth">宽</label>
          <input id="boardWidth" type="number" min="1" class="size-input" />
          <label for="boardHeight" style="margin-left: 6px">高</label>
          <input id="boardHeight" type="number" min="1" class="size-input" />
        </div>
        <div class="count-with-help">
          <div id="count">0 张图片</div>
          <button id="shortcutHelp" class="icon-btn" title="快捷键与设置">
            ⌨️
          </button>
        </div>
      </div>
    </div>

    <!-- UI 显隐切换按钮（右下角） -->
    <button
      id="uiToggle"
      class="icon-btn ui-toggle"
      title="隐藏工具栏与状态栏"
      aria-pressed="true"
    >
      👁️
    </button>

    <!-- 右键菜单 -->
    <div id="ctxMenu" class="context-menu" role="menu">
      <div
        style="
          margin: 4px 6px 8px;
          color: var(--text-secondary);
          font-size: 0.9rem;
        "
      >
        绘图工具
      </div>
      <button id="ctxToolMove">拖动</button>
      <button id="ctxToolBrush">画笔</button>
      <button id="ctxToolArrow">箭头</button>
      <button id="ctxToolEllipse">椭圆</button>
      <button id="ctxToolRect">画框</button>
      <button id="ctxToolText">文字</button>
      <div style="height: 8px"></div>
      <button id="ctxUndo">撤销</button>
      <div style="height: 8px"></div>
      <button id="ctxSave">保存</button>
      <button id="ctxCopyPNG">写入剪贴板</button>
      <button id="ctxScan">识别二维码</button>
      <div style="height: 8px"></div>
      <button id="ctxDelete">删除</button>
    </div>

    <!-- 快捷键设置弹框 -->
    <div
      id="shortcutModal"
      class="shortcut-modal"
      role="dialog"
      aria-modal="true"
      aria-labelledby="shortcutModalTitle"
    >
      <div class="modal-content" onclick="event.stopPropagation()">
        <div class="modal-header">
          <h3 id="shortcutModalTitle">快捷键说明与设置</h3>
          <button id="shortcutClose" class="icon-btn" title="关闭">✕</button>
        </div>
        <div
          style="
            margin-bottom: 10px;
            color: var(--text-secondary);
            font-size: 0.95rem;
          "
        >
          当前工具快捷键：画笔、箭头、椭圆、画框、文字。可点击“捕获”记录新组合并保存到本地。
        </div>
        <div class="shortcut-list">
          <div class="label">画笔</div>
          <input id="sc_brush" class="shortcut-input" type="text" readonly />
          <button class="icon-btn capture-btn" data-tool="brush">捕获</button>
          <button class="icon-btn clear-btn" data-tool="brush">清除</button>

          <div class="label">箭头</div>
          <input id="sc_arrow" class="shortcut-input" type="text" readonly />
          <button class="icon-btn capture-btn" data-tool="arrow">捕获</button>
          <button class="icon-btn clear-btn" data-tool="arrow">清除</button>

          <div class="label">椭圆</div>
          <input id="sc_ellipse" class="shortcut-input" type="text" readonly />
          <button class="icon-btn capture-btn" data-tool="ellipse">捕获</button>
          <button class="icon-btn clear-btn" data-tool="ellipse">清除</button>

          <div class="label">画框</div>
          <input id="sc_rect" class="shortcut-input" type="text" readonly />
          <button class="icon-btn capture-btn" data-tool="rect">捕获</button>
          <button class="icon-btn clear-btn" data-tool="rect">清除</button>

          <div class="label">文字</div>
          <input id="sc_text" class="shortcut-input" type="text" readonly />
          <button class="icon-btn capture-btn" data-tool="text">捕获</button>
          <button class="icon-btn clear-btn" data-tool="text">清除</button>
        </div>
        <div class="modal-actions">
          <button id="shortcutReset" class="btn-danger">恢复默认</button>
          <button id="shortcutSave" class="btn-primary">保存到本地</button>
        </div>
        <div class="shortcut-tip">
          默认快捷键为 Ctrl+Alt+1/2/3/4/5（避开 Ctrl+W/T/R
          等浏览器保留）。如需自定义，推荐使用 Alt 或 Shift 组合或功能键。
        </div>
      </div>
    </div>

    <script src="./text-to-image/html-to-image.min.js"></script>
    <script src="./qrcode/jsQR.min.js"></script>
    <script>
      // 上线日期（遵循功能卡片的 launchDate 规则：当天时间 + 颜文字）
      ;(function setLaunchDate() {
        const el = document.getElementById('launchDate')
        const today = new Date()
        const yyyy = today.getFullYear()
        const mm = String(today.getMonth() + 1).padStart(2, '0')
        const dd = String(today.getDate()).padStart(2, '0')
        const kaomoji = [
          '(＾▽＾)',
          '(•‿•)',
          '(￣▽￣)',
          '(o^▽^o)',
          '(๑˃ᴗ˂)ﻭ',
          '( ´ ▽ ` )ﾉ',
        ]
        const k = kaomoji[Math.floor(Math.random() * kaomoji.length)]
        el.textContent = `上线日期: ${yyyy}-${mm}-${dd} ${k}`
      })()

      const board = document.getElementById('board')
      const boardContent = document.getElementById('boardContent')
      const statusEl = document.getElementById('status')
      const countEl = document.getElementById('count')
      const btnImport = document.getElementById('btnImport')
      const btnExport = document.getElementById('btnExport')
      const btnClear = document.getElementById('btnClear')
      const fileInput = document.getElementById('fileInput')
      const exportMode = document.getElementById('exportMode')
      const ctxMenu = document.getElementById('ctxMenu')
      const ctxDelete = document.getElementById('ctxDelete')
      const zoomIndicator = document.getElementById('zoomIndicator')
      const btnResetZoom = document.getElementById('btnResetZoom')
      const boardWidthInput = document.getElementById('boardWidth')
      const boardHeightInput = document.getElementById('boardHeight')
      const statusbarEl = document.querySelector('.statusbar')
      const uiToggle = document.getElementById('uiToggle')
      const ctxToolMove = document.getElementById('ctxToolMove')
      const ctxToolRect = document.getElementById('ctxToolRect')
      const ctxToolText = document.getElementById('ctxToolText')
      // 新增工具按钮
      const ctxToolBrush = document.getElementById('ctxToolBrush')
      const ctxToolArrow = document.getElementById('ctxToolArrow')
      const ctxToolEllipse = document.getElementById('ctxToolEllipse')
      const ctxUndo = document.getElementById('ctxUndo')
      const ctxSave = document.getElementById('ctxSave')
      const ctxCopyPNG = document.getElementById('ctxCopyPNG')
      const ctxScan = document.getElementById('ctxScan')
      const shortcutHelpBtn = document.getElementById('shortcutHelp')
      const shortcutModal = document.getElementById('shortcutModal')
      const shortcutCloseBtn = document.getElementById('shortcutClose')
      const shortcutSaveBtn = document.getElementById('shortcutSave')
      const shortcutResetBtn = document.getElementById('shortcutReset')
      const btnExportSelected = document.getElementById('btnExportSelected')
      const toolbarEl = document.querySelector('.toolbar')
      const textConfigPanel = document.getElementById('textConfigPanel')
      const textFontSizeInput = document.getElementById('textFontSize')
      const textColorInput = document.getElementById('textColor')
      // 内联操作栏元素
      const textInlineToolbar = document.getElementById('textInlineToolbar')
      const inlineFontSize = document.getElementById('inlineFontSize')
      const inlineColor = document.getElementById('inlineColor')
      // 记忆最近一次位于文本项内的选区（用于面板操作）
      let lastTextSelection = null

      let zCounter = 1
      const items = new Set()
      const objectUrls = new Set()
      // 虚拟画板尺寸（用于“画板尺寸”导出），默认当前窗口尺寸
      let virtualBoardWidth = Math.round(window.innerWidth)
      let virtualBoardHeight = Math.round(window.innerHeight)
      // 画板平移与拖拽模式状态
      let boardTranslateX = 0
      let boardTranslateY = 0
      let boardScale = 1
      const MIN_SCALE = 0.25
      const MAX_SCALE = 4
      let isPanMode = false
      let isPanning = false
      let panKeyTimer = null
      let panStartX = 0
      let panStartY = 0
      let panBaseX = 0
      let panBaseY = 0
      let selectedItem = null
      let selectedGroup = []
      // Ctrl+C/Ctrl+V 内部复制粘贴：支持图片与所有工具项
      let copiedImage = null
      let copiedItems = null
      let currentTool = 'move' // move|doodle|circle|rect|text
      let drawingState = null
      let suppressBoardClickReset = false
      let uiVisible = true

      // 文字配置面板样式与定位
      ;(function initTextConfigPanelStyles() {
        const style = document.createElement('style')
        style.textContent = `
          .text-config-panel {
            position: fixed;
            left: 50%;
            transform: translateX(-50%);
            z-index: 1000;
            background: var(--primary-bg);
            border-radius: 18px;
            padding: 10px 12px;
            box-shadow: 8px 8px 16px var(--shadow-dark),
              -8px -8px 16px var(--shadow-light);
          }
          .text-config-inner {
            display: flex;
            align-items: center;
            gap: 12px;
          }
          .text-config-panel .cfg-item {
            display: inline-flex;
            align-items: center;
            gap: 8px;
            background: var(--primary-bg);
            color: var(--text-primary);
            padding: 6px 10px;
            border-radius: 12px;
            box-shadow: inset 2px 2px 6px var(--shadow-dark), inset -2px -2px 6px var(--shadow-light);
          }
          .text-config-panel label {
            font-size: 0.9rem;
            color: var(--text-secondary);
          }
          #textFontSize {
            width: 80px;
            padding: 6px 8px;
            border-radius: 8px;
            border: 1px solid rgba(var(--accent-rgb), 0.2);
            background: var(--secondary-bg);
            color: var(--text-primary);
          }
          #textColor {
            width: 36px;
            height: 24px;
            border: none;
            padding: 0;
            border-radius: 6px;
            background: transparent;
            cursor: pointer;
            box-shadow: inset 4px 4px 8px var(--shadow-dark), inset -4px -4px 8px var(--shadow-light);
          }

          /* 文本内联工具栏样式（显示在选中文本元素下方） */
          .text-inline-toolbar {
            position: fixed;
            z-index: 1001;
            background: var(--primary-bg);
            color: var(--text-primary);
            border-radius: 12px;
            padding: 8px 10px;
            box-shadow: 8px 8px 16px var(--shadow-dark),
              -8px -8px 16px var(--shadow-light);
            /* 避免遮挡画板拖动/框选：容器不拦截指针事件 */
            pointer-events: none;
          }
          .text-inline-toolbar .inline-inner {
            display: inline-flex;
            align-items: center;
            gap: 12px;
            /* 仅输入控件可交互，其余不拦截事件 */
            pointer-events: none;
          }
          .text-inline-toolbar .inline-item {
            display: inline-flex;
            align-items: center;
            gap: 8px;
            background: var(--primary-bg);
            padding: 6px 10px;
            border-radius: 10px;
            box-shadow: inset 2px 2px 6px var(--shadow-dark), inset -2px -2px 6px var(--shadow-light);
            pointer-events: none;
          }
          .text-inline-toolbar .inline-label {
            font-size: 0.85rem;
            color: var(--text-secondary);
            pointer-events: none;
          }
          #inlineFontSize {
            width: 72px;
            padding: 6px 8px;
            border-radius: 8px;
            border: 1px solid rgba(var(--accent-rgb), 0.2);
            background: var(--secondary-bg);
            color: var(--text-primary);
            pointer-events: auto;
          }
          #inlineColor {
            width: 36px;
            height: 24px;
            border: none;
            padding: 0;
            border-radius: 6px;
            background: transparent;
            cursor: pointer;
            box-shadow: inset 4px 4px 8px var(--shadow-dark), inset -4px -4px 8px var(--shadow-light);
            pointer-events: auto;
          }
        `
        document.head.appendChild(style)
      })()

      function positionTextConfigPanel() {
        if (!toolbarEl || !textConfigPanel) return
        const rect = toolbarEl.getBoundingClientRect()
        textConfigPanel.style.top = Math.round(rect.bottom + 8) + 'px'
      }
      window.addEventListener('resize', positionTextConfigPanel)
      positionTextConfigPanel()

      function rgbToHex(rgb) {
        const m = String(rgb)
          .replace(/\s+/g, '')
          .match(/^rgba?\((\d+),(\d+),(\d+)/i)
        if (!m) return '#000000'
        const r = Math.max(0, Math.min(255, parseInt(m[1], 10)))
        const g = Math.max(0, Math.min(255, parseInt(m[2], 10)))
        const b = Math.max(0, Math.min(255, parseInt(m[3], 10)))
        const toHex = (n) => n.toString(16).padStart(2, '0')
        return `#${toHex(r)}${toHex(g)}${toHex(b)}`
      }

      function syncTextConfigUI() {
        if (!selectedItem || !selectedItem.classList.contains('text-item'))
          return
        const textEl = selectedItem.querySelector('.text-content')
        if (!textEl) return
        // 优先使用最近一次记忆的选区样式；否则读取整段样式
        let cs = getComputedStyle(textEl)
        if (lastTextSelection && lastTextSelection.el === textEl) {
          let node = lastTextSelection.range.startContainer
          if (node && node.nodeType === Node.TEXT_NODE)
            node = node.parentElement
          if (node && node instanceof Element && textEl.contains(node)) {
            cs = getComputedStyle(node)
          }
        }
        const fs = parseInt(cs.fontSize, 10) || 16
        textFontSizeInput.value = String(fs)
        const colorHex = rgbToHex(cs.color)
        textColorInput.value = colorHex
      }

      function updateTextConfigVisibility() {
        const singleSelectedText =
          !!selectedItem && selectedItem.classList.contains('text-item')
        const showPanel =
          singleSelectedText &&
          (currentTool === 'text' || currentTool === 'move')
        // 旧面板不再展示
        textConfigPanel.style.display = 'none'
      }

      // 内联操作栏：根据选中状态与工具显示，跟随“选中文本项容器”定位在其下方
      function positionInlineToolbar() {
        if (!textInlineToolbar) return
        if (!selectedItem || !selectedItem.classList.contains('text-item'))
          return
        // 锚定到“选中文本项容器”的矩形
        const anchorRect = selectedItem.getBoundingClientRect()

        const desiredLeft = Math.round(anchorRect.left)
        // 默认在容器下方额外留出 30px 间距，避免覆盖拖动/缩放手柄区域
        const desiredTop = Math.round(anchorRect.bottom + 30)

        // 估计宽度用于防止越界
        const prevDisplay = textInlineToolbar.style.display
        if (prevDisplay === 'none') {
          textInlineToolbar.style.visibility = 'hidden'
          textInlineToolbar.style.display = 'block'
        }
        const width = textInlineToolbar.offsetWidth || 220
        const maxLeft = Math.max(8, window.innerWidth - width - 8)
        textInlineToolbar.style.display = prevDisplay
        textInlineToolbar.style.visibility = ''
        textInlineToolbar.style.left =
          Math.min(Math.max(8, desiredLeft), maxLeft) + 'px'
        textInlineToolbar.style.top = desiredTop + 'px'
      }

      function syncInlineTextUI() {
        if (!inlineFontSize || !inlineColor) return
        if (!selectedItem || !selectedItem.classList.contains('text-item'))
          return
        const textEl = selectedItem.querySelector('.text-content')
        if (!textEl) return
        let cs = getComputedStyle(textEl)
        if (lastTextSelection && lastTextSelection.el === textEl) {
          let node = lastTextSelection.range.startContainer
          if (node && node.nodeType === Node.TEXT_NODE)
            node = node.parentElement
          if (node && node instanceof Element && textEl.contains(node)) {
            cs = getComputedStyle(node)
          }
        }
        const fs = parseInt(cs.fontSize, 10) || 16
        inlineFontSize.value = String(fs)
        const colorHex = rgbToHex(cs.color)
        inlineColor.value = colorHex
      }

      function updateTextInlineToolbarVisibility() {
        if (!textInlineToolbar) return
        const singleSelectedText =
          !!selectedItem && selectedItem.classList.contains('text-item')
        const showBar =
          singleSelectedText &&
          (currentTool === 'text' || currentTool === 'move')
        if (showBar) {
          textInlineToolbar.style.display = 'block'
          syncInlineTextUI()
          positionInlineToolbar()
        } else {
          textInlineToolbar.style.display = 'none'
        }
      }

      // 监听选区变化：当选区位于某个文本项内时进行记忆（点击面板后仍可使用该选区）
      // 只在“非折叠、且位于文本项内”的选区变化时记录选区
      document.addEventListener('selectionchange', () => {
        try {
          const sel = window.getSelection?.()
          if (!sel || sel.rangeCount === 0) return
          const r = sel.getRangeAt(0)
          if (r.collapsed) return
          let node = r.commonAncestorContainer
          if (node && node.nodeType === Node.TEXT_NODE)
            node = node.parentElement
          const textEl =
            node && node.closest ? node.closest('.text-content') : null
          if (textEl && selectedItem && selectedItem.contains(textEl)) {
            const saved = document.createRange()
            saved.setStart(r.startContainer, r.startOffset)
            saved.setEnd(r.endContainer, r.endOffset)
            lastTextSelection = { el: textEl, range: saved }
            // 选区变化后，若工具栏可见，立即跟随选区刷新定位
            positionInlineToolbar()
          }
        } catch (_) {}
      })

      function saveCurrentTextSelection() {
        try {
          const sel = window.getSelection?.()
          if (!sel || sel.rangeCount === 0) return
          const r = sel.getRangeAt(0)
          if (r.collapsed) return
          let node = r.commonAncestorContainer
          if (node && node.nodeType === Node.TEXT_NODE)
            node = node.parentElement
          const textEl =
            node && node.closest ? node.closest('.text-content') : null
          if (textEl && selectedItem && selectedItem.contains(textEl)) {
            const saved = document.createRange()
            saved.setStart(r.startContainer, r.startOffset)
            saved.setEnd(r.endContainer, r.endOffset)
            lastTextSelection = { el: textEl, range: saved }
          }
        } catch (_) {}
      }

      // 将样式应用到指定选区（优先使用记忆的选区）；若无选区返回 false
      function applyStyleToRange(textEl, range, styleObj) {
        try {
          if (!range || range.collapsed) return false
          if (
            !textEl.contains(range.startContainer) ||
            !textEl.contains(range.endContainer)
          )
            return false
          const frag = range.cloneContents()
          if (!frag || !frag.textContent || !frag.textContent.length)
            return false
          const wrapper = document.createElement('span')
          if (styleObj) {
            Object.entries(styleObj).forEach(([k, v]) => {
              try {
                wrapper.style[k] = v
              } catch (_) {}
            })
          }
          wrapper.appendChild(frag)
          range.deleteContents()
          range.insertNode(wrapper)
          // 调整光标：置于包裹内容末尾（在包裹内部继续输入，继承同样样式）
          try {
            const after = document.createRange()
            after.selectNodeContents(wrapper)
            after.collapse(false)
            const sel = window.getSelection?.()
            sel && (sel.removeAllRanges(), sel.addRange(after))
            // 保留最近一次“非折叠”的选区，不被覆盖
            lastTextSelection = { el: textEl, range: after }
          } catch (_) {}
          // 规范化文本节点，避免碎片化
          try {
            textEl.normalize()
          } catch (_) {}
          return true
        } catch (_) {
          return false
        }
      }

      function applyStyleToTextSelection(textEl, styleObj) {
        const saved = lastTextSelection
        if (saved && saved.el === textEl) {
          return applyStyleToRange(textEl, saved.range, styleObj)
        }
        return false
      }

      // 整段应用字号：设置容器字号并清理子节点的字体大小内联样式，使其继承
      function applyFontSizeToWholeText(textEl, px) {
        try {
          if (!textEl) return
          textEl.style.fontSize =
            typeof px === 'number' ? px + 'px' : String(px)
          const descendants = textEl.querySelectorAll('*')
          descendants.forEach((el) => {
            try {
              if (el && el.style) {
                el.style.fontSize = ''
              }
            } catch (_) {}
          })
          try {
            textEl.normalize()
          } catch (_) {}
        } catch (_) {}
      }

      // 整段应用颜色：设置容器颜色并清理子节点的颜色内联样式，使其继承
      function applyColorToWholeText(textEl, color) {
        try {
          if (!textEl) return
          textEl.style.color = String(color)
          const descendants = textEl.querySelectorAll('*')
          descendants.forEach((el) => {
            try {
              if (el && el.style) {
                el.style.color = ''
              }
            } catch (_) {}
          })
          try {
            textEl.normalize()
          } catch (_) {}
        } catch (_) {}
      }

      // 面板交互：字号与颜色变更应用到当前选中文本
      // 在面板或内联栏交互前先保存当前文本选区，避免因失焦导致选区丢失
      textFontSizeInput?.addEventListener('mousedown', saveCurrentTextSelection)
      textColorInput?.addEventListener('mousedown', saveCurrentTextSelection)
      inlineFontSize?.addEventListener('mousedown', saveCurrentTextSelection)
      inlineColor?.addEventListener('mousedown', saveCurrentTextSelection)

      textFontSizeInput?.addEventListener('input', () => {
        if (!selectedItem || !selectedItem.classList.contains('text-item'))
          return
        const textEl = selectedItem.querySelector('.text-content')
        if (!textEl) return
        const v = Math.max(
          10,
          Math.min(200, parseInt(textFontSizeInput.value || '16', 10))
        )
        // 优先作用于选区；若无选区则作用于整段
        const applied = applyStyleToTextSelection(textEl, {
          fontSize: v + 'px',
        })
        if (!applied) {
          // 无有效选区时，整段应用并清理子节点字号覆盖，确保生效
          applyFontSizeToWholeText(textEl, v)
        }
        // 字体大小变化后，实时刷新内联工具栏位置
        positionInlineToolbar()
      })

      textColorInput?.addEventListener('input', () => {
        if (!selectedItem || !selectedItem.classList.contains('text-item'))
          return
        const textEl = selectedItem.querySelector('.text-content')
        if (!textEl) return
        const val = textColorInput.value
        if (!val) return
        // 优先作用于选区；若无选区则作用于整段
        // 在颜色选择后，优先使用之前记录的非折叠选区
        const applied = applyStyleToTextSelection(textEl, { color: val })
        if (!applied) {
          // 无有效选区时，整段应用并清理子节点颜色覆盖，确保新输入继承
          applyColorToWholeText(textEl, val)
        }
        // 颜色变化可能影响文本度量，刷新内联工具栏位置
        positionInlineToolbar()
      })

      // 内联操作栏事件：字号与颜色
      inlineFontSize?.addEventListener('input', () => {
        if (!selectedItem || !selectedItem.classList.contains('text-item'))
          return
        const textEl = selectedItem.querySelector('.text-content')
        if (!textEl) return
        const v = Math.max(
          10,
          Math.min(200, parseInt(inlineFontSize.value || '16', 10))
        )
        const applied = applyStyleToTextSelection(textEl, {
          fontSize: v + 'px',
        })
        if (!applied) {
          // 无有效选区时，整段应用并清理子节点字号覆盖，确保生效
          applyFontSizeToWholeText(textEl, v)
        }
        // 内联栏修改字号后，实时刷新工具栏位置
        positionInlineToolbar()
      })

      inlineColor?.addEventListener('input', () => {
        if (!selectedItem || !selectedItem.classList.contains('text-item'))
          return
        const textEl = selectedItem.querySelector('.text-content')
        if (!textEl) return
        const val = inlineColor.value
        if (!val) return
        const applied = applyStyleToTextSelection(textEl, { color: val })
        if (!applied) {
          // 无有效选区时，整段应用并清理子节点颜色覆盖，确保新输入继承
          applyColorToWholeText(textEl, val)
        }
        // 内联栏修改颜色后，实时刷新工具栏位置
        positionInlineToolbar()
      })

      // 视口变化时更新内联操作栏位置
      window.addEventListener('resize', () => positionInlineToolbar())

      // 快捷键设置：默认组合与加载/保存
      const SHORTCUTS_STORAGE_KEY = 'canvasBoard.shortcuts'
      const defaultShortcuts = {
        // 默认采用 Ctrl+Alt+1..5，避开浏览器保留的 Ctrl+W/T/R 等
        brush: { ctrl: true, alt: true, shift: false, key: '1' },
        arrow: { ctrl: true, alt: true, shift: false, key: '2' },
        ellipse: { ctrl: true, alt: true, shift: false, key: '3' },
        rect: { ctrl: true, alt: true, shift: false, key: '4' },
        text: { ctrl: true, alt: true, shift: false, key: '5' },
      }
      let shortcuts = { ...defaultShortcuts }

      function formatShortcut(sc) {
        if (!sc || !sc.key) return '未设置'
        const parts = []
        if (sc.ctrl) parts.push('Ctrl')
        if (sc.alt) parts.push('Alt')
        if (sc.shift) parts.push('Shift')
        parts.push((sc.key || '').toUpperCase())
        return parts.join('+')
      }
      function isValidKey(key) {
        if (!key) return false
        const k = String(key).toLowerCase()
        if (/^[a-z0-9]$/.test(k)) return true
        if (/^f([1-9]|1[0-2])$/i.test(key)) return true
        return false
      }
      function normalizeShortcut(sc) {
        if (!sc || !isValidKey(sc.key)) return null
        return {
          ctrl: !!sc.ctrl,
          alt: !!sc.alt,
          shift: !!sc.shift,
          key: String(sc.key).toLowerCase(),
        }
      }
      function isBrowserReservedShortcut(sc) {
        if (!sc || !sc.key) return false
        const key = String(sc.key).toLowerCase()
        // 常见浏览器保留快捷键
        if (sc.ctrl && ['w', 't', 'r', 'n', 'p'].includes(key)) return true
        if (sc.alt && key === 'f4') return true
        return false
      }
      function loadShortcuts() {
        try {
          const raw = localStorage.getItem(SHORTCUTS_STORAGE_KEY)
          if (raw) {
            const data = JSON.parse(raw)
            const out = {}
            ;['brush', 'arrow', 'ellipse', 'rect', 'text'].forEach((t) => {
              out[t] = normalizeShortcut(data[t]) || defaultShortcuts[t]
              if (isBrowserReservedShortcut(out[t])) {
                out[t] = defaultShortcuts[t]
              }
            })
            shortcuts = out
          } else {
            shortcuts = { ...defaultShortcuts }
          }
        } catch (e) {
          console.warn('加载快捷键失败，使用默认', e)
          shortcuts = { ...defaultShortcuts }
        }
      }
      function saveShortcuts() {
        localStorage.setItem(SHORTCUTS_STORAGE_KEY, JSON.stringify(shortcuts))
        setStatus('快捷键已保存到本地')
      }
      function updateShortcutInputs() {
        const map = {
          brush: document.getElementById('sc_brush'),
          arrow: document.getElementById('sc_arrow'),
          ellipse: document.getElementById('sc_ellipse'),
          rect: document.getElementById('sc_rect'),
          text: document.getElementById('sc_text'),
        }
        Object.entries(map).forEach(([tool, input]) => {
          if (input) input.value = formatShortcut(shortcuts[tool])
        })
      }
      loadShortcuts()
      requestAnimationFrame(updateShortcutInputs)

      // 弹框开关
      shortcutHelpBtn?.addEventListener('click', () => {
        shortcutModal.style.display = 'flex'
      })
      shortcutCloseBtn?.addEventListener('click', () => {
        shortcutModal.style.display = 'none'
      })
      shortcutModal?.addEventListener('click', () => {
        shortcutModal.style.display = 'none'
      })

      // 捕获模式
      let capturingTool = null
      function beginCapture(tool) {
        capturingTool = tool
        setStatus(`正在捕获 ${tool} 的快捷键…请按下组合键`)
      }
      function endCapture() {
        capturingTool = null
      }
      document.addEventListener('keydown', (e) => {
        if (!capturingTool) return
        e.preventDefault()
        e.stopPropagation()
        const key = String(e.key || '').toLowerCase()
        const sc = {
          ctrl: !!e.ctrlKey,
          alt: !!e.altKey,
          shift: !!e.shiftKey,
          key,
        }
        if (!isValidKey(sc.key)) {
          setStatus('该键不支持，请使用字母/数字或 F1-F12')
          return
        }
        if (isBrowserReservedShortcut(sc)) {
          setStatus('该组合为浏览器保留快捷键，建议使用单键或 Alt/Shift 组合')
          return
        }
        shortcuts[capturingTool] = normalizeShortcut(sc)
        updateShortcutInputs()
        setStatus(`已设置 ${capturingTool} 快捷键为 ${formatShortcut(sc)}`)
        endCapture()
      })
      document.querySelectorAll('.capture-btn').forEach((btn) => {
        btn.addEventListener('click', () => {
          const tool = btn.getAttribute('data-tool')
          beginCapture(tool)
        })
      })
      document.querySelectorAll('.clear-btn').forEach((btn) => {
        btn.addEventListener('click', () => {
          const tool = btn.getAttribute('data-tool')
          shortcuts[tool] = { ctrl: false, alt: false, shift: false, key: '' }
          updateShortcutInputs()
          setStatus(`已清除 ${tool} 快捷键`)
        })
      })
      shortcutSaveBtn?.addEventListener('click', () => {
        saveShortcuts()
      })
      shortcutResetBtn?.addEventListener('click', () => {
        shortcuts = { ...defaultShortcuts }
        updateShortcutInputs()
        saveShortcuts()
        setStatus('快捷键已恢复为默认组合')
      })

      // 快捷键触发工具切换（避开文本输入场景）
      function matchShortcut(e, sc) {
        if (!sc || !sc.key) return false
        const key = String(e.key || '').toLowerCase()
        return (
          !!sc.ctrl === !!e.ctrlKey &&
          !!sc.alt === !!e.altKey &&
          !!sc.shift === !!e.shiftKey &&
          key === String(sc.key).toLowerCase()
        )
      }
      document.addEventListener('keydown', (e) => {
        if (capturingTool) return
        if (isTextInputTarget(e.target)) return
        const order = [
          ['brush', 'brush'],
          ['arrow', 'arrow'],
          ['ellipse', 'ellipse'],
          ['rect', 'rect'],
          ['text', 'text'],
        ]
        for (const [tool, arg] of order) {
          if (matchShortcut(e, shortcuts[tool])) {
            setTool(arg)
            hideContextMenu()
            e.preventDefault()
            e.stopPropagation()
            break
          }
        }
      })

      // 统一更新画板的变换：先平移后缩放（平移为屏幕像素）
      function updateBoardTransform() {
        // 注意：transform 列表按右到左应用；使用 translate(...) scale(...) 保证平移不被缩放倍乘
        boardContent.style.transform = `translate(${boardTranslateX}px, ${boardTranslateY}px) scale(${boardScale})`
        // 缩放/平移变化时，若有选中文本，实时刷新内联工具栏位置
        updateTextInlineToolbarVisibility()
      }
      updateBoardTransform()

      function updateZoomIndicator() {
        if (!zoomIndicator) return
        const pct = Math.round(boardScale * 100)
        zoomIndicator.textContent = `缩放 ${pct}%`
      }
      updateZoomIndicator()

      // UI 显隐控制
      function setUIVisible(visible) {
        uiVisible = !!visible
        toolbarEl?.classList.toggle('hidden', !uiVisible)
        statusbarEl?.classList.toggle('hidden', !uiVisible)
        if (uiToggle) {
          uiToggle.textContent = uiVisible ? '👁️' : '🙈'
          uiToggle.title = uiVisible
            ? '隐藏工具栏与状态栏'
            : '显示工具栏与状态栏'
          uiToggle.setAttribute('aria-pressed', uiVisible ? 'true' : 'false')
        }
      }
      uiToggle?.addEventListener('click', () => setUIVisible(!uiVisible))
      // 默认展示
      setUIVisible(true)

      // 初始化画板尺寸输入
      if (boardWidthInput && boardHeightInput) {
        boardWidthInput.value = String(virtualBoardWidth)
        boardHeightInput.value = String(virtualBoardHeight)
      }

      function updateCount() {
        countEl.textContent = `${items.size} 张图片`
      }

      function setStatus(msg) {
        statusEl.textContent = msg
      }

      // 缩放：在鼠标位置进行缩放，保持该位置的内容不漂移
      function zoomAt(clientX, clientY, factor) {
        const rect = board.getBoundingClientRect()
        const sx = clamp(boardScale * factor, MIN_SCALE, MAX_SCALE)
        const px = clientX - rect.left
        const py = clientY - rect.top
        // 当前指针下的内容坐标
        const cx = (px - boardTranslateX) / boardScale
        const cy = (py - boardTranslateY) / boardScale
        // 新平移，使该内容坐标在新缩放后仍落在指针下
        boardScale = sx
        boardTranslateX = px - boardScale * cx
        boardTranslateY = py - boardScale * cy
        updateBoardTransform()
        updateZoomIndicator()
        setStatus(`缩放至 ${Math.round(boardScale * 100)}%`)
      }

      function showContextMenu(x, y) {
        // 先显示再计算尺寸，避免溢出屏幕
        ctxMenu.style.display = 'block'
        let left = x
        let top = y
        const rect = ctxMenu.getBoundingClientRect()
        const pad = 8
        if (left + rect.width > window.innerWidth - pad) {
          left = Math.max(pad, window.innerWidth - rect.width - pad)
        }
        if (top + rect.height > window.innerHeight - pad) {
          top = Math.max(pad, window.innerHeight - rect.height - pad)
        }
        ctxMenu.style.left = left + 'px'
        ctxMenu.style.top = top + 'px'
      }
      function hideContextMenu() {
        ctxMenu.style.display = 'none'
      }

      // Ctrl+滚轮缩放（以指针为中心）
      board.addEventListener(
        'wheel',
        (e) => {
          if (!e.ctrlKey) return
          e.preventDefault()
          const factor = e.deltaY < 0 ? 1.1 : 0.9
          zoomAt(e.clientX, e.clientY, factor)
        },
        { passive: false }
      )

      // 缩放重置按钮与快捷键
      btnResetZoom?.addEventListener('click', () => {
        boardScale = 1
        boardTranslateX = 0
        boardTranslateY = 0
        updateBoardTransform()
        updateZoomIndicator()
        setStatus('已重置缩放为 100%')
      })
      document.addEventListener('keydown', (e) => {
        if (capturingTool) return
        if (isTextInputTarget(e.target)) return
        if (e.ctrlKey && (e.key === '+' || e.key === '=')) {
          e.preventDefault()
          zoomAt(
            e.clientX || window.innerWidth / 2,
            e.clientY || window.innerHeight / 2,
            1.1
          )
        } else if (e.ctrlKey && (e.key === '-' || e.key === '_')) {
          e.preventDefault()
          zoomAt(
            e.clientX || window.innerWidth / 2,
            e.clientY || window.innerHeight / 2,
            0.9
          )
        } else if (e.ctrlKey && (e.key === '0' || e.code === 'Digit0')) {
          e.preventDefault()
          boardScale = 1
          boardTranslateX = 0
          boardTranslateY = 0
          updateBoardTransform()
          updateZoomIndicator()
          setStatus('已重置缩放为 100%')
        }
      })

      function setTool(tool) {
        // 别名映射：brush -> doodle, ellipse -> circle
        if (tool === 'brush') tool = 'doodle'
        if (tool === 'ellipse') tool = 'circle'
        currentTool = tool
        const map = {
          move: '拖动',
          doodle: '涂鸦',
          brush: '画笔',
          circle: '画圈',
          ellipse: '椭圆',
          rect: '画框',
          text: '文字',
          arrow: '箭头',
        }
        setStatus(`工具：${map[tool] || tool}`)
        hideContextMenu()
        // 在非拖动模式下，确保绘制元素置于图片之上
        if (currentTool !== 'move') raiseOverlayLayers()
        // 工具切换时更新文字配置面板可见性
        updateTextConfigVisibility()
        // 同步内联工具栏的显示与定位
        updateTextInlineToolbarVisibility()
      }

      function createItem(src, naturalWidth, naturalHeight) {
        const item = document.createElement('div')
        item.className = 'item'
        item.style.zIndex = ++zCounter // 新增默认层级高一点

        const img = new Image()
        img.src = src

        // 依据当前画板可视宽高（结合缩放）等比缩放，避免初始尺寸超出视口
        const naturalW = Math.max(1, Math.round(naturalWidth || 200))
        const naturalH = Math.max(1, Math.round(naturalHeight || 200))
        const viewW = Math.max(1, board.clientWidth)
        const viewH = Math.max(1, board.clientHeight)
        // 当前缩放下，内容的 CSS 尺寸上限（确保 w*boardScale<=viewW 等）
        const maxCssW = viewW / Math.max(0.0001, boardScale)
        const maxCssH = viewH / Math.max(0.0001, boardScale)
        const fitScale = Math.min(1, maxCssW / naturalW, maxCssH / naturalH)
        const w = Math.max(60, Math.round(naturalW * fitScale))
        const h = Math.max(60, Math.round(naturalH * fitScale))

        item.style.width = w + 'px'
        item.style.height = h + 'px'
        // 初始居中位置需考虑当前内容偏移
        item.style.left =
          Math.round((board.clientWidth - w) / 2 - boardTranslateX) + 'px'
        item.style.top =
          Math.round((board.clientHeight - h) / 2 - boardTranslateY) + 'px'

        item.appendChild(img)

        // 四角手柄
        ;['nw', 'ne', 'sw', 'se'].forEach((dir) => {
          const handle = document.createElement('div')
          handle.className = `handle ${dir}`
          handle.dataset.dir = dir
          item.appendChild(handle)
        })

        boardContent.appendChild(item)
        items.add(item)
        updateCount()

        // 交互绑定
        bindItemInteraction(item)
        return item
      }

      function bindItemInteraction(item) {
        function bringToFront() {
          // 提升当前项层级；若为图片，则同时确保所有操作 SVG/形状等覆盖其上
          const isImage = !!item.querySelector('img')
          item.style.zIndex = ++zCounter
          if (isImage) {
            // 保证：未选图片 < 选中图片 < 操作 SVG（始终在最上层）
            raiseOverlayLayers()
          }
        }

        item.addEventListener('pointerdown', (e) => {
          if (e.button !== 0) return
          // 空格拖拽模式优先：拦截项交互改为拖动画板
          if (isPanMode || panKeyTimer) {
            // 若用户已按下空格但尚未到达长按阈值，立即进入空格拖拽模式
            if (panKeyTimer) {
              clearTimeout(panKeyTimer)
              panKeyTimer = null
              isPanMode = true
              board.classList.add('pan-mode')
              const selectedCountNow = Array.from(items).filter((it) =>
                it.classList.contains('selected')
              ).length
              if (selectedCountNow > 1) {
                setStatus('按住空格拖动选中对象（松开退出）')
              } else {
                setStatus('按住空格拖动画板（松开退出）')
              }
            }
            const selectedCount = Array.from(items).filter((it) =>
              it.classList.contains('selected')
            ).length
            // 有多选则空格拖动选中元素组
            if (selectedCount > 1) {
              e.preventDefault()
              e.stopPropagation()
              startGroupDrag(e)
              return
            }
            // 否则保持空格拖动画板
            e.preventDefault()
            e.stopPropagation()
            isPanning = true
            board.classList.add('pan-active')
            panStartX = e.clientX
            panStartY = e.clientY
            panBaseX = boardTranslateX
            panBaseY = boardTranslateY
            const onMove = (ev) => {
              const dx = ev.clientX - panStartX
              const dy = ev.clientY - panStartY
              boardTranslateX = panBaseX + dx
              boardTranslateY = panBaseY + dy
              updateBoardTransform()
            }
            const onUp = () => {
              isPanning = false
              board.classList.remove('pan-active')
              document.removeEventListener('pointermove', onMove)
              document.removeEventListener('pointerup', onUp)
            }
            document.addEventListener('pointermove', onMove)
            document.addEventListener('pointerup', onUp)
            return
          }
          if (e.target.classList.contains('handle')) return // 由专用逻辑处理
          // Ctrl+点击：进行多选切换（不清空其他选中）
          if (e.ctrlKey) {
            e.stopPropagation()
            const wasSelected = item.classList.contains('selected')
            if (wasSelected) {
              item.classList.remove('selected')
            } else {
              item.classList.add('selected')
            }
            // 同步选中组变量
            const selectedEls = Array.from(items).filter((it) =>
              it.classList.contains('selected')
            )
            if (selectedEls.length === 1) {
              selectedItem = selectedEls[0]
              selectedGroup = []
            } else if (selectedEls.length > 1) {
              selectedItem = null
              selectedGroup = selectedEls
            } else {
              selectedItem = null
              selectedGroup = []
            }
            updateTextEditableState()
            hideContextMenu()
            setStatus(
              `多选：当前选中 ${selectedEls.length} 个对象${
                wasSelected ? '（已取消一个）' : ''
              }`
            )
            return
          }
          // 普通点击：若已有多选并且点击的是其中之一，则保持多选以便整体拖动
          const selectedCountBefore = Array.from(items).filter((it) =>
            it.classList.contains('selected')
          ).length
          const clickedIsSelected = item.classList.contains('selected')
          if (selectedCountBefore > 1 && clickedIsSelected) {
            hideContextMenu()
            // 仅在“拖动”工具下允许拖动
            if (currentTool !== 'move') return
            e.preventDefault()
            let marqueeStarted = false
            const startX = e.clientX
            const startY = e.clientY
            const selectedCount = selectedCountBefore
            const onMove = (ev) => {
              if (marqueeStarted) return
              const dist = Math.hypot(ev.clientX - startX, ev.clientY - startY)
              if (dist > 4) {
                clearTimeout(timer)
                document.removeEventListener('pointermove', onMove)
                document.removeEventListener('pointerup', onUp)
                bringToFront()
                if (selectedCount > 1) {
                  startGroupDrag(e)
                } else {
                  startDrag(e, item)
                }
              }
            }
            const onUp = () => {
              clearTimeout(timer)
              document.removeEventListener('pointermove', onMove)
              document.removeEventListener('pointerup', onUp)
            }
            const timer = setTimeout(() => {
              marqueeStarted = true
              document.removeEventListener('pointermove', onMove)
              document.removeEventListener('pointerup', onUp)
              beginMarquee(e)
            }, 200)
            document.addEventListener('pointermove', onMove)
            document.addEventListener('pointerup', onUp)
            return
          }
          // 左键点击选中并显示手柄（会清空其他选中）
          selectItem(item)
          // 选择项时关闭右键菜单
          hideContextMenu()
          // 仅在“拖动”工具下允许拖动
          if (currentTool !== 'move') {
            // 保持默认行为，例如文本可获得焦点
            return
          }
          e.preventDefault()
          let marqueeStarted = false
          const startX = e.clientX
          const startY = e.clientY
          const selectedCount = Array.from(items).filter((it) =>
            it.classList.contains('selected')
          ).length
          const onMove = (ev) => {
            if (marqueeStarted) return
            const dist = Math.hypot(ev.clientX - startX, ev.clientY - startY)
            if (dist > 4) {
              clearTimeout(timer)
              document.removeEventListener('pointermove', onMove)
              document.removeEventListener('pointerup', onUp)
              bringToFront()
              if (selectedCount > 1 && item.classList.contains('selected')) {
                startGroupDrag(e)
              } else {
                startDrag(e, item)
              }
            }
          }
          const onUp = () => {
            clearTimeout(timer)
            document.removeEventListener('pointermove', onMove)
            document.removeEventListener('pointerup', onUp)
          }
          const timer = setTimeout(() => {
            marqueeStarted = true
            document.removeEventListener('pointermove', onMove)
            document.removeEventListener('pointerup', onUp)
            beginMarquee(e)
          }, 200)
          document.addEventListener('pointermove', onMove)
          document.addEventListener('pointerup', onUp)
        })

        // 文本项双击：进入编辑并默认全选现有文字，便于直接替换
        item.addEventListener('dblclick', (e) => {
          const textEl = item.querySelector('.text-content')
          if (!textEl) return
          e.preventDefault()
          e.stopPropagation()
          selectItem(item)
          // 进入编辑焦点
          textEl.focus()
          // 选中全部内容
          try {
            const range = document.createRange()
            range.selectNodeContents(textEl)
            const sel = window.getSelection()
            sel.removeAllRanges()
            sel.addRange(range)
          } catch (_) {
            // 兼容性容错：失败时退化为 execCommand
            try {
              document.execCommand('selectAll', false, undefined)
            } catch (_) {}
          }
          setStatus('已进入文字编辑，默认全选内容')
        })

        // 文本编辑时实时刷新内联工具栏位置（输入会改变元素尺寸）
        const textElForEdit = item.querySelector('.text-content')
        if (textElForEdit) {
          textElForEdit.addEventListener('input', () => {
            if (selectedItem === item) positionInlineToolbar()
          })
        }

        // 角点缩放
        item.querySelectorAll('.handle').forEach((h) => {
          h.addEventListener('pointerdown', (e) => {
            // 允许在“拖动”与“文字”工具下缩放
            if (!(currentTool === 'move' || currentTool === 'text')) return
            e.stopPropagation()
            e.preventDefault()
            bringToFront()
            selectItem(item)
            startResize(e, item, h.dataset.dir)
          })
        })
      }

      // 提升所有非图片元素层级，使其位于图片之上（仅在非“拖动”模式下调用）
      function raiseOverlayLayers() {
        Array.from(items).forEach((el) => {
          const isImage = !!el.querySelector('img')
          if (!isImage) {
            el.style.zIndex = ++zCounter
          }
        })
      }

      function addHandles(item) {
        ;['nw', 'ne', 'sw', 'se'].forEach((dir) => {
          const handle = document.createElement('div')
          handle.className = `handle ${dir}`
          handle.dataset.dir = dir
          item.appendChild(handle)
        })
      }

      function selectItem(item) {
        items.forEach((it) => it.classList.remove('selected'))
        item.classList.add('selected')
        selectedItem = item
        selectedGroup = []
        updateTextEditableState()
        updateTextConfigVisibility()
        updateTextInlineToolbarVisibility()
      }

      function setGroupSelection(list) {
        items.forEach((it) => it.classList.remove('selected'))
        list.forEach((it) => it.classList.add('selected'))
        selectedItem = null
        selectedGroup = list
        updateTextEditableState()
        updateTextConfigVisibility()
        updateTextInlineToolbarVisibility()
      }

      function clearSelection() {
        items.forEach((it) => it.classList.remove('selected'))
        selectedItem = null
        selectedGroup = []
        updateTextEditableState()
        updateTextConfigVisibility()
        updateTextInlineToolbarVisibility()
      }

      // 根据选中状态切换文本项的可编辑性：仅选中项允许编辑
      function updateTextEditableState() {
        items.forEach((it) => {
          const textEl = it.querySelector('.text-content')
          if (!textEl) return
          const editable = it.classList.contains('selected')
          textEl.contentEditable = editable ? 'true' : 'false'
        })
        // 同步文字配置面板显示逻辑
        updateTextConfigVisibility()
      }

      function clamp(val, min, max) {
        return Math.max(min, Math.min(max, val))
      }

      function startDrag(e, item) {
        // 拖动时默认开启辅助线显示（避免 Alt 状态残留导致隐藏）
        altPressed = false
        const startX = e.clientX
        const startY = e.clientY
        const startLeft = item.offsetLeft
        const startTop = item.offsetTop

        item.setPointerCapture?.(e.pointerId)

        const onMove = (ev) => {
          const dx = (ev.clientX - startX) / boardScale
          const dy = (ev.clientY - startY) / boardScale
          let newLeft = startLeft + dx
          let newTop = startTop + dy
          // 仅对图片项启用吸附
          const isImageItem = !!item.querySelector('img')
          if (isImageItem) {
            const itemRect = {
              left: newLeft,
              top: newTop,
              right: newLeft + item.offsetWidth,
              bottom: newTop + item.offsetHeight,
              cx: newLeft + item.offsetWidth / 2,
              cy: newTop + item.offsetHeight / 2,
            }
            let bestX = null
            let bestXDist = Infinity
            let bestY = null
            let bestYDist = Infinity

            const others = Array.from(items).filter(
              (it) => it !== item && !!it.querySelector('img')
            )
            for (const other of others) {
              const o = {
                left: other.offsetLeft,
                top: other.offsetTop,
                right: other.offsetLeft + other.offsetWidth,
                bottom: other.offsetTop + other.offsetHeight,
                cx: other.offsetLeft + other.offsetWidth / 2,
                cy: other.offsetTop + other.offsetHeight / 2,
              }
              // X 轴候选：中心、左右边对齐（含左-右、右-左）
              const xCandidates = [
                {
                  pos: o.cx,
                  val: o.cx - item.offsetWidth / 2,
                  dist: Math.abs(itemRect.cx - o.cx),
                  guide: 'center',
                },
                {
                  pos: o.left,
                  val: o.left,
                  dist: Math.abs(itemRect.left - o.left),
                  guide: 'left',
                },
                {
                  pos: o.right,
                  val: o.right - item.offsetWidth,
                  dist: Math.abs(itemRect.right - o.right),
                  guide: 'right',
                },
                {
                  pos: o.right,
                  val: o.right,
                  dist: Math.abs(itemRect.left - o.right),
                  guide: 'right',
                },
                {
                  pos: o.left,
                  val: o.left - item.offsetWidth,
                  dist: Math.abs(itemRect.right - o.left),
                  guide: 'left',
                },
              ]
              for (const c of xCandidates) {
                if (c.dist < bestXDist) {
                  bestXDist = c.dist
                  bestX = c
                }
              }
              // Y 轴候选：中心、上下边对齐（含上-下、下-上）
              const yCandidates = [
                {
                  pos: o.cy,
                  val: o.cy - item.offsetHeight / 2,
                  dist: Math.abs(itemRect.cy - o.cy),
                  guide: 'center',
                },
                {
                  pos: o.top,
                  val: o.top,
                  dist: Math.abs(itemRect.top - o.top),
                  guide: 'top',
                },
                {
                  pos: o.bottom,
                  val: o.bottom - item.offsetHeight,
                  dist: Math.abs(itemRect.bottom - o.bottom),
                  guide: 'bottom',
                },
                {
                  pos: o.bottom,
                  val: o.bottom,
                  dist: Math.abs(itemRect.top - o.bottom),
                  guide: 'bottom',
                },
                {
                  pos: o.top,
                  val: o.top - item.offsetHeight,
                  dist: Math.abs(itemRect.bottom - o.top),
                  guide: 'top',
                },
              ]
              for (const c of yCandidates) {
                if (c.dist < bestYDist) {
                  bestYDist = c.dist
                  bestY = c
                }
              }
            }
            // 应用阈值内的最优吸附，并显示辅助线
            const snapThr = snapThreshold / boardScale
            if (bestX && bestXDist <= snapThr) {
              newLeft = bestX.val
              showGuideX(bestX.pos)
            } else {
              if (guideXEl) guideXEl.style.display = 'none'
            }
            if (bestY && bestYDist <= snapThr) {
              newTop = bestY.val
              showGuideY(bestY.pos)
            } else {
              if (guideYEl) guideYEl.style.display = 'none'
            }
          }
          item.style.left = newLeft + 'px'
          item.style.top = newTop + 'px'
          // 拖动过程中，若当前拖动的是选中文本项，则实时刷新工具栏位置
          if (selectedItem === item && item.classList.contains('text-item')) {
            positionInlineToolbar()
          }
        }
        const onUp = () => {
          document.removeEventListener('pointermove', onMove)
          document.removeEventListener('pointerup', onUp)
          item.releasePointerCapture?.(e.pointerId)
          hideGuides()
          const endLeft = item.offsetLeft
          const endTop = item.offsetTop
          if (endLeft !== startLeft || endTop !== startTop) {
            pushHistory({
              type: 'move',
              els: [item],
              from: [{ left: startLeft, top: startTop }],
              to: [{ left: endLeft, top: endTop }],
            })
          }
        }
        document.addEventListener('pointermove', onMove)
        document.addEventListener('pointerup', onUp)
      }

      function startGroupDrag(e) {
        // 组拖动时默认开启辅助线显示（避免 Alt 状态残留导致隐藏）
        altPressed = false
        const selectedEls = Array.from(items).filter((it) =>
          it.classList.contains('selected')
        )
        if (!selectedEls.length) return
        const startX = e.clientX
        const startY = e.clientY
        const starts = selectedEls.map((el) => ({
          el,
          left: el.offsetLeft,
          top: el.offsetTop,
        }))
        // 捕获指针由当前触发项完成即可
        const capEl = e.currentTarget
        capEl?.setPointerCapture?.(e.pointerId)
        const onMove = (ev) => {
          const dx = (ev.clientX - startX) / boardScale
          const dy = (ev.clientY - startY) / boardScale
          const moveX = Math.abs(dx)
          const moveY = Math.abs(dy)
          // 以组内“图片元素”为准进行吸附：沿移动方向选择最近的图片作为锚点
          const groupImageStarts = starts.filter(
            (s) => !!s.el.querySelector('img')
          )
          let snapDeltaX = 0
          let snapDeltaY = 0
          let bestX = null
          let bestXDist = Infinity
          let bestY = null
          let bestYDist = Infinity
          // 仅当组内存在图片元素时启用吸附参考
          if (!altPressed && groupImageStarts.length) {
            // 选取水平锚点：dx>0 取组内最右；dx<0 取最左
            let anchorXStart = null
            if (dx > 0) {
              for (const s of groupImageStarts) {
                if (
                  !anchorXStart ||
                  s.left + s.el.offsetWidth >
                    anchorXStart.left + anchorXStart.el.offsetWidth
                ) {
                  anchorXStart = s
                }
              }
            } else if (dx < 0) {
              for (const s of groupImageStarts) {
                if (!anchorXStart || s.left < anchorXStart.left) {
                  anchorXStart = s
                }
              }
            }
            // 选取垂直锚点：dy>0 取组内最下；dy<0 取最上
            let anchorYStart = null
            if (dy > 0) {
              for (const s of groupImageStarts) {
                if (
                  !anchorYStart ||
                  s.top + s.el.offsetHeight >
                    anchorYStart.top + anchorYStart.el.offsetHeight
                ) {
                  anchorYStart = s
                }
              }
            } else if (dy < 0) {
              for (const s of groupImageStarts) {
                if (!anchorYStart || s.top < anchorYStart.top) {
                  anchorYStart = s
                }
              }
            }
            // 组外图片作为候选对齐对象
            const selectedSet = new Set(selectedEls)
            const others = Array.from(items).filter(
              (it) => !selectedSet.has(it) && !!it.querySelector('img')
            )
            // 计算水平吸附候选
            if (anchorXStart && others.length) {
              const itemRect = {
                left: anchorXStart.left + dx,
                right: anchorXStart.left + dx + anchorXStart.el.offsetWidth,
                cx: anchorXStart.left + dx + anchorXStart.el.offsetWidth / 2,
              }
              for (const other of others) {
                const o = {
                  left: other.offsetLeft,
                  right: other.offsetLeft + other.offsetWidth,
                  cx: other.offsetLeft + other.offsetWidth / 2,
                }
                const xCandidates = [
                  {
                    pos: o.cx,
                    val: o.cx - anchorXStart.el.offsetWidth / 2,
                    dist: Math.abs(itemRect.cx - o.cx),
                    guide: 'center',
                  },
                  {
                    pos: o.left,
                    val: o.left,
                    dist: Math.abs(itemRect.left - o.left),
                    guide: 'left',
                  },
                  {
                    pos: o.right,
                    val: o.right - anchorXStart.el.offsetWidth,
                    dist: Math.abs(itemRect.right - o.right),
                    guide: 'right',
                  },
                  {
                    pos: o.right,
                    val: o.right,
                    dist: Math.abs(itemRect.left - o.right),
                    guide: 'right',
                  },
                  {
                    pos: o.left,
                    val: o.left - anchorXStart.el.offsetWidth,
                    dist: Math.abs(itemRect.right - o.left),
                    guide: 'left',
                  },
                ]
                for (const c of xCandidates) {
                  if (c.dist < bestXDist) {
                    bestXDist = c.dist
                    bestX = c
                  }
                }
              }
              const snapThr = snapThreshold / boardScale
              if (bestX && bestXDist <= snapThr && moveX > 2) {
                const originLeft = anchorXStart.left
                // 避免吸附回到自身初始位置导致“卡住”
                if (Math.abs(bestX.val - originLeft) > 0.5) {
                  snapDeltaX = bestX.val - (anchorXStart.left + dx)
                  showGuideX(bestX.pos)
                } else {
                  if (guideXEl) guideXEl.style.display = 'none'
                }
              } else {
                if (guideXEl) guideXEl.style.display = 'none'
              }
            }
            // 计算垂直吸附候选
            if (anchorYStart && others.length) {
              const itemRect = {
                top: anchorYStart.top + dy,
                bottom: anchorYStart.top + dy + anchorYStart.el.offsetHeight,
                cy: anchorYStart.top + dy + anchorYStart.el.offsetHeight / 2,
              }
              for (const other of others) {
                const o = {
                  top: other.offsetTop,
                  bottom: other.offsetTop + other.offsetHeight,
                  cy: other.offsetTop + other.offsetHeight / 2,
                }
                const yCandidates = [
                  {
                    pos: o.cy,
                    val: o.cy - anchorYStart.el.offsetHeight / 2,
                    dist: Math.abs(itemRect.cy - o.cy),
                    guide: 'center',
                  },
                  {
                    pos: o.top,
                    val: o.top,
                    dist: Math.abs(itemRect.top - o.top),
                    guide: 'top',
                  },
                  {
                    pos: o.bottom,
                    val: o.bottom - anchorYStart.el.offsetHeight,
                    dist: Math.abs(itemRect.bottom - o.bottom),
                    guide: 'bottom',
                  },
                  {
                    pos: o.bottom,
                    val: o.bottom,
                    dist: Math.abs(itemRect.top - o.bottom),
                    guide: 'bottom',
                  },
                  {
                    pos: o.top,
                    val: o.top - anchorYStart.el.offsetHeight,
                    dist: Math.abs(itemRect.bottom - o.top),
                    guide: 'top',
                  },
                ]
                for (const c of yCandidates) {
                  if (c.dist < bestYDist) {
                    bestYDist = c.dist
                    bestY = c
                  }
                }
              }
              const snapThr = snapThreshold / boardScale
              if (bestY && bestYDist <= snapThr && moveY > 2) {
                const originTop = anchorYStart.top
                // 避免吸附回到自身初始位置导致“卡住”
                if (Math.abs(bestY.val - originTop) > 0.5) {
                  snapDeltaY = bestY.val - (anchorYStart.top + dy)
                  showGuideY(bestY.pos)
                } else {
                  if (guideYEl) guideYEl.style.display = 'none'
                }
              } else {
                if (guideYEl) guideYEl.style.display = 'none'
              }
            }
          } else {
            // Alt 按住或无图片锚点：不做吸附，仅隐藏辅助线
            if (guideXEl) guideXEl.style.display = 'none'
            if (guideYEl) guideYEl.style.display = 'none'
          }
          // 应用吸附偏移到组内所有元素
          starts.forEach((s) => {
            const newLeft = s.left + dx + (snapDeltaX || 0)
            const newTop = s.top + dy + (snapDeltaY || 0)
            s.el.style.left = newLeft + 'px'
            s.el.style.top = newTop + 'px'
          })
        }
        const onUp = () => {
          document.removeEventListener('pointermove', onMove)
          document.removeEventListener('pointerup', onUp)
          hideGuides()
          capEl?.releasePointerCapture?.(e.pointerId)
          const from = starts.map((s) => ({ left: s.left, top: s.top }))
          const to = starts.map((s) => ({
            left: s.el.offsetLeft,
            top: s.el.offsetTop,
          }))
          const moved = to.some(
            (p, i) => p.left !== from[i].left || p.top !== from[i].top
          )
          if (moved) {
            pushHistory({
              type: 'move',
              els: starts.map((s) => s.el),
              from,
              to,
            })
          }
        }
        document.addEventListener('pointermove', onMove)
        document.addEventListener('pointerup', onUp)
      }

      function startResize(e, item, dir) {
        const startX = e.clientX
        const startY = e.clientY
        const startLeft = item.offsetLeft
        const startTop = item.offsetTop
        const startW = item.offsetWidth
        const startH = item.offsetHeight

        item.setPointerCapture?.(e.pointerId)

        const minSize = 40
        const onMove = (ev) => {
          const dx = (ev.clientX - startX) / boardScale
          const dy = (ev.clientY - startY) / boardScale
          let left = startLeft,
            top = startTop,
            w = startW,
            h = startH

          if (dir === 'nw') {
            w = Math.max(minSize, startW - dx)
            h = Math.max(minSize, startH - dy)
            left = startLeft + dx
            top = startTop + dy
          }
          if (dir === 'ne') {
            w = Math.max(minSize, startW + dx)
            h = Math.max(minSize, startH - dy)
            top = startTop + dy
          }
          if (dir === 'sw') {
            w = Math.max(minSize, startW - dx)
            h = Math.max(minSize, startH + dy)
            left = startLeft + dx
          }
          if (dir === 'se') {
            w = Math.max(minSize, startW + dx)
            h = Math.max(minSize, startH + dy)
          }

          item.style.width = w + 'px'
          item.style.height = h + 'px'
          item.style.left = left + 'px'
          item.style.top = top + 'px'
          // 缩放过程中，若该项为当前选中的文本项，则实时刷新工具栏位置
          if (selectedItem === item && item.classList.contains('text-item')) {
            positionInlineToolbar()
          }
        }
        const onUp = () => {
          document.removeEventListener('pointermove', onMove)
          document.removeEventListener('pointerup', onUp)
          item.releasePointerCapture?.(e.pointerId)
          const end = {
            left: item.offsetLeft,
            top: item.offsetTop,
            width: item.offsetWidth,
            height: item.offsetHeight,
          }
          const start = {
            left: startLeft,
            top: startTop,
            width: startW,
            height: startH,
          }
          if (
            end.left !== start.left ||
            end.top !== start.top ||
            end.width !== start.width ||
            end.height !== start.height
          ) {
            pushHistory({ type: 'resize', el: item, from: start, to: end })
          }
        }
        document.addEventListener('pointermove', onMove)
        document.addEventListener('pointerup', onUp)
      }

      // 已移除 Ctrl+滚轮缩放画板逻辑

      // 已移除缩放重置逻辑

      // 在画板区域内统一管理右键：
      // - 当右键对象为任意“项”（图片、文字、形状、涂鸦、箭头等）时显示自定义菜单
      // - 空白处仍不显示菜单
      board.addEventListener('contextmenu', (e) => {
        e.preventDefault()
        const itemEl = e.target.closest('.item')
        if (!itemEl) {
          hideContextMenu()
          return
        }
        // 右键不触发选中状态（不显示手柄），但记录当前项用于菜单操作
        selectedItem = itemEl
        items.forEach((it) => it.classList.remove('selected'))
        showContextMenu(e.clientX, e.clientY)
      })

      async function addImageFromFile(file) {
        return new Promise((resolve, reject) => {
          const reader = new FileReader()
          reader.onload = () => {
            const dataUrl = reader.result
            const img = new Image()
            img.onload = () => {
              const item = createItem(
                dataUrl,
                img.naturalWidth,
                img.naturalHeight
              )
              setStatus(
                `已导入：${file.name} (${Math.round(file.size / 1024)} KB)`
              )
              resolve(item)
            }
            img.onerror = (e) => {
              console.error('图片加载失败', e)
              setStatus('图片读取失败')
              reject(e)
            }
            img.src = dataUrl
          }
          reader.onerror = (e) => {
            console.error('文件读取失败', e)
            setStatus('文件读取失败')
            reject(e)
          }
          reader.readAsDataURL(file)
        })
      }

      function clearBoard() {
        items.forEach((item) => item.remove())
        items.clear()
        updateCount()
        setStatus('画板已清空')
      }

      // 历史栈：记录新增/删除/移动/缩放等操作，支持撤销/重做
      const history = []
      const redoStack = []
      function pushHistory(action) {
        history.push(action)
        redoStack.length = 0
      }
      function undoAction() {
        const last = history.pop()
        if (!last) {
          setStatus('无可撤销操作')
          return
        }
        if (last.type === 'add') {
          if (items.has(last.el)) items.delete(last.el)
          last.el.remove()
        } else if (last.type === 'remove') {
          boardContent.appendChild(last.el)
          items.add(last.el)
          bindItemInteraction(last.el)
        } else if (last.type === 'removeGroup') {
          // 组删除撤销：全部复原
          last.els.forEach((el) => {
            boardContent.appendChild(el)
            items.add(el)
            bindItemInteraction(el)
          })
        } else if (last.type === 'move') {
          // 移动撤销：恢复起始位置
          last.els.forEach((el, i) => {
            const pos = last.from[i]
            el.style.left = pos.left + 'px'
            el.style.top = pos.top + 'px'
          })
        } else if (last.type === 'resize') {
          // 缩放撤销：恢复起始尺寸与位置
          const f = last.from
          last.el.style.left = f.left + 'px'
          last.el.style.top = f.top + 'px'
          last.el.style.width = f.width + 'px'
          last.el.style.height = f.height + 'px'
        }
        redoStack.push(last)
        updateCount()
        setStatus('已撤销')
      }
      function redoAction() {
        const act = redoStack.pop()
        if (!act) {
          setStatus('无可重做操作')
          return
        }
        if (act.type === 'add') {
          boardContent.appendChild(act.el)
          items.add(act.el)
          bindItemInteraction(act.el)
        } else if (act.type === 'remove') {
          if (items.has(act.el)) items.delete(act.el)
          act.el.remove()
        } else if (act.type === 'removeGroup') {
          // 组删除重做：全部删除
          act.els.forEach((el) => {
            if (items.has(el)) items.delete(el)
            el.remove()
          })
        } else if (act.type === 'move') {
          // 移动重做：应用目标位置
          act.els.forEach((el, i) => {
            const pos = act.to[i]
            el.style.left = pos.left + 'px'
            el.style.top = pos.top + 'px'
          })
        } else if (act.type === 'resize') {
          // 缩放重做：应用目标尺寸与位置
          const t = act.to
          act.el.style.left = t.left + 'px'
          act.el.style.top = t.top + 'px'
          act.el.style.width = t.width + 'px'
          act.el.style.height = t.height + 'px'
        }
        history.push(act)
        updateCount()
        setStatus('已重做')
      }

      // 文件选择
      btnImport.addEventListener('click', () => fileInput.click())
      fileInput.addEventListener('change', async (e) => {
        const files = Array.from(e.target.files || [])
        if (!files.length) return
        for (const f of files) {
          if (f.type.startsWith('image/')) await addImageFromFile(f)
        }
        fileInput.value = ''
      })

      // 拖拽到画板
      board.addEventListener('dragover', (e) => {
        e.preventDefault()
      })
      board.addEventListener('drop', async (e) => {
        e.preventDefault()
        const files = Array.from(e.dataTransfer?.files || [])
        if (!files.length) return
        for (const f of files) {
          if (f.type.startsWith('image/')) await addImageFromFile(f)
        }
      })

      // 粘贴图片（Ctrl+V）
      document.addEventListener('paste', async (e) => {
        const itemsList = Array.from(e.clipboardData?.items || [])
        const imgItems = itemsList.filter(
          (it) => it.type && it.type.startsWith('image/')
        )
        if (!imgItems.length) return
        for (const it of imgItems) {
          const file = it.getAsFile()
          if (file) await addImageFromFile(file)
        }
        setStatus(`已粘贴 ${imgItems.length} 张图片`)
      })

      // 点击空白画板：重置为“拖动”并清空选中状态（框选结束后的下一次点击不触发）
      board.addEventListener('click', (e) => {
        if (suppressBoardClickReset) {
          suppressBoardClickReset = false
          return
        }
        // 若当前正在编辑文字，则不清空也不切换工具
        if (currentTool === 'text') {
          const active = document.activeElement
          if (
            active &&
            active.classList &&
            active.classList.contains('text-content')
          ) {
            return
          }
        }
        // 只在点击空白区域（不含任何项）时触发
        if (e.target.closest('.item')) return
        clearSelection()
        hideContextMenu()
        // 始终切换到“拖动”模式，避免误创建
        setTool('move')
        setStatus('已重置为拖动模式，已清空选中')
      })

      // 全局快捷键：撤销/重做与图片复制/粘贴
      document.addEventListener(
        'keydown',
        (e) => {
          const key = (e.key || '').toLowerCase()
          const ctrlOrMeta = e.ctrlKey || e.metaKey

          // 撤销：Ctrl+Z / ⌘+Z
          if (ctrlOrMeta && !e.shiftKey && key === 'z') {
            e.preventDefault()
            undoAction()
            return
          }

          // 重做：Ctrl+Y 或 Shift+Ctrl+Z / Shift+⌘+Z
          if (
            (ctrlOrMeta && key === 'y') ||
            (ctrlOrMeta && e.shiftKey && key === 'z')
          ) {
            e.preventDefault()
            redoAction()
            return
          }

          // 复制：支持单选或多选（图片、形状、文字、涂鸦、箭头）
          if (ctrlOrMeta && !e.shiftKey && key === 'c') {
            // 在输入/可编辑区域内，保留原生复制行为
            if (isTextInputTarget(e.target)) return
            const groupNow = Array.from(items).filter((it) =>
              it.classList.contains('selected')
            )
            if (!groupNow.length) {
              setStatus('请先选中对象')
              return
            }
            e.preventDefault()
            // 若仅有一个且为图片，同时保留旧的图片复制数据，便于与系统剪贴板区分
            if (groupNow.length === 1) {
              const only = groupNow[0]
              const imgEl = only.querySelector('img')
              if (imgEl) {
                copiedImage = {
                  src: imgEl.src,
                  naturalWidth:
                    imgEl.naturalWidth || imgEl.width || only.offsetWidth,
                  naturalHeight:
                    imgEl.naturalHeight || imgEl.height || only.offsetHeight,
                  width: only.offsetWidth,
                  height: only.offsetHeight,
                  left: only.offsetLeft,
                  top: only.offsetTop,
                }
              } else {
                copiedImage = null
              }
            } else {
              // 多选时不使用旧的 copiedImage
              copiedImage = null
            }
            // 记录当前所选项引用用于内部粘贴克隆
            copiedItems = groupNow.slice()
            setStatus(`已复制 ${groupNow.length} 个对象`)
            return
          }

          // 粘贴：优先使用内部复制的对象组，其次回退到旧的图片复制
          if (ctrlOrMeta && !e.shiftKey && key === 'v') {
            // 在输入/可编辑区域内，保留原生粘贴文本行为
            if (isTextInputTarget(e.target)) return
            // 若有内部复制的对象集合，批量克隆
            if (copiedItems && copiedItems.length) {
              e.preventDefault()
              const clones = []
              const baseOffset = 12
              copiedItems.forEach((srcEl) => {
                const clone = srcEl.cloneNode(true)
                clone.classList.remove('selected')
                // 位置偏移，避免完全重叠
                const left = (srcEl.offsetLeft || 0) + baseOffset
                const top = (srcEl.offsetTop || 0) + baseOffset
                clone.style.left = left + 'px'
                clone.style.top = top + 'px'
                clone.style.zIndex = ++zCounter
                boardContent.appendChild(clone)
                items.add(clone)
                bindItemInteraction(clone)
                clones.push(clone)
                pushHistory({ type: 'add', el: clone })
              })
              if (clones.length > 1) setGroupSelection(clones)
              else if (clones.length === 1) selectItem(clones[0])
              setStatus(`已粘贴 ${clones.length} 个对象`)
              return
            }
            // 兼容：仅复制了图片的旧逻辑
            if (copiedImage) {
              e.preventDefault()
              const item = createItem(
                copiedImage.src,
                copiedImage.naturalWidth,
                copiedImage.naturalHeight
              )
              item.style.width = copiedImage.width + 'px'
              item.style.height = copiedImage.height + 'px'
              const baseLeft = selectedItem
                ? selectedItem.offsetLeft
                : copiedImage.left || 0
              const baseTop = selectedItem
                ? selectedItem.offsetTop
                : copiedImage.top || 0
              const left = baseLeft + 12
              const top = baseTop + 12
              item.style.left = left + 'px'
              item.style.top = top + 'px'
              selectItem(item)
              pushHistory({ type: 'add', el: item })
              setStatus('已粘贴复制图片')
              return
            }
          }

          // 删除：Delete / Backspace（非输入控件时生效）
          if (!ctrlOrMeta && (key === 'delete' || key === 'backspace')) {
            if (isTextInputTarget(e.target)) return
            e.preventDefault()
            // 组删除优先
            const group = Array.from(items).filter((it) =>
              it.classList.contains('selected')
            )
            if (group.length > 1) {
              const els = group.slice()
              els.forEach((el) => {
                el.remove()
                items.delete(el)
              })
              pushHistory({ type: 'removeGroup', els })
              clearSelection()
              updateCount()
              setStatus(`已删除 ${els.length} 个选中对象`)
              return
            }
            // 单个删除
            if (selectedItem && items.has(selectedItem)) {
              const el = selectedItem
              el.remove()
              items.delete(el)
              pushHistory({ type: 'remove', el })
              selectedItem = null
              updateCount()
              setStatus('已删除选中对象')
              return
            }
          }
        },
        { capture: true }
      )

      // 获取画板坐标
      function getBoardPoint(ev) {
        const r = board.getBoundingClientRect()
        return {
          x: ev.clientX - r.left,
          y: ev.clientY - r.top,
        }
      }

      // 获取内容容器坐标（考虑内容平移偏移）
      function getContentPoint(ev) {
        const r = board.getBoundingClientRect()
        const sx = ev.clientX - r.left
        const sy = ev.clientY - r.top
        return {
          x: (sx - boardTranslateX) / boardScale,
          y: (sy - boardTranslateY) / boardScale,
        }
      }

      // 通用框选启动：在任意位置（包括项上）长按触发
      function beginMarquee(startEvent) {
        const start = getBoardPoint(startEvent)
        clearSelection()
        const marqueeEl = document.createElement('div')
        marqueeEl.className = 'marquee'
        marqueeEl.style.left = start.x + 'px'
        marqueeEl.style.top = start.y + 'px'
        marqueeEl.style.width = '1px'
        marqueeEl.style.height = '1px'
        board.appendChild(marqueeEl)
        const onMove = (ev) => {
          const p = getBoardPoint(ev)
          const left = Math.min(start.x, p.x)
          const top = Math.min(start.y, p.y)
          const w = Math.max(1, Math.abs(p.x - start.x))
          const h = Math.max(1, Math.abs(p.y - start.y))
          marqueeEl.style.left = left + 'px'
          marqueeEl.style.top = top + 'px'
          marqueeEl.style.width = w + 'px'
          marqueeEl.style.height = h + 'px'
        }
        const onUp = () => {
          document.removeEventListener('pointermove', onMove)
          document.removeEventListener('pointerup', onUp)
          const mRect = marqueeEl.getBoundingClientRect()
          marqueeEl.remove()
          // 优化框选：若框选区域未完全包含图片，但与图片有交叠，则仅选择该图片内部的其他元素
          const allItems = Array.from(items)
          const overlappedImages = allItems.filter((it) => {
            if (!it.querySelector('img')) return false
            const ir = it.getBoundingClientRect()
            return !(
              ir.right < mRect.left ||
              ir.left > mRect.right ||
              ir.bottom < mRect.top ||
              ir.top > mRect.bottom
            )
          })
          const fullyCoveredImages = overlappedImages.filter((it) => {
            const ir = it.getBoundingClientRect()
            return (
              mRect.left <= ir.left &&
              mRect.right >= ir.right &&
              mRect.top <= ir.top &&
              mRect.bottom >= ir.bottom
            )
          })
          let selected = null
          if (overlappedImages.length && fullyCoveredImages.length === 0) {
            // 仅选择图片内部的非图片元素（且与框选区域相交）
            selected = allItems.filter((it) => {
              if (it.querySelector('img')) return false
              const ir = it.getBoundingClientRect()
              const intersect = !(
                ir.right < mRect.left ||
                ir.left > mRect.right ||
                ir.bottom < mRect.top ||
                ir.top > mRect.bottom
              )
              if (!intersect) return false
              // 必须位于任一交叠图片内部
              return overlappedImages.some((img) => {
                const r = img.getBoundingClientRect()
                return (
                  ir.left >= r.left &&
                  ir.right <= r.right &&
                  ir.top >= r.top &&
                  ir.bottom <= r.bottom
                )
              })
            })
          } else {
            // 默认：选取与框选区域相交的所有对象（含图片）
            selected = allItems.filter((it) => {
              const ir = it.getBoundingClientRect()
              return !(
                ir.right < mRect.left ||
                ir.left > mRect.right ||
                ir.bottom < mRect.top ||
                ir.top > mRect.bottom
              )
            })
          }
          if (selected.length) {
            setGroupSelection(selected)
            setStatus(`已框选 ${selected.length} 个对象，可整体拖动`)
          } else {
            clearSelection()
            setStatus('未框选到对象')
          }
          // 屏蔽框选结束触发的后续一次空白点击重置
          suppressBoardClickReset = true
          setTimeout(() => (suppressBoardClickReset = false), 0)
        }
        document.addEventListener('pointermove', onMove)
        document.addEventListener('pointerup', onUp)
      }

      // 左键在画板或图片上根据当前工具创建图形/文字/涂鸦/箭头/马赛克
      board.addEventListener('pointerdown', (e) => {
        if (e.button !== 0) return
        // 空格长按进入拖拽画板模式，优先处理拖拽
        if (isPanMode || panKeyTimer) {
          // 若用户已按下空格但尚未到达长按阈值，立即进入空格拖拽模式
          if (panKeyTimer) {
            clearTimeout(panKeyTimer)
            panKeyTimer = null
            isPanMode = true
            board.classList.add('pan-mode')
            const selectedCountNow = Array.from(items).filter((it) =>
              it.classList.contains('selected')
            ).length
            if (selectedCountNow > 1) {
              setStatus('按住空格拖动选中对象（松开退出）')
            } else {
              setStatus('按住空格拖动画板（松开退出）')
            }
          }
          e.preventDefault()
          // 有多选则空格拖动选中元素组，否则拖动画板
          const selectedCount = Array.from(items).filter((it) =>
            it.classList.contains('selected')
          ).length
          if (selectedCount > 1) {
            startGroupDrag(e)
            return
          }
          isPanning = true
          board.classList.add('pan-active')
          panStartX = e.clientX
          panStartY = e.clientY
          panBaseX = boardTranslateX
          panBaseY = boardTranslateY
          const onMove = (ev) => {
            const dx = ev.clientX - panStartX
            const dy = ev.clientY - panStartY
            boardTranslateX = panBaseX + dx
            boardTranslateY = panBaseY + dy
            updateBoardTransform()
          }
          const onUp = () => {
            isPanning = false
            board.classList.remove('pan-active')
            document.removeEventListener('pointermove', onMove)
            document.removeEventListener('pointerup', onUp)
          }
          document.addEventListener('pointermove', onMove)
          document.addEventListener('pointerup', onUp)
          return
        }
        // “拖动”工具下：长按空白处进入框选
        if (currentTool === 'move') {
          const targetItem = e.target.closest('.item')
          if (targetItem) return // 项交互由项自身处理
          e.preventDefault()
          const start = getBoardPoint(e)
          let marqueeEl = null
          let started = false
          const timer = setTimeout(() => {
            started = true
            marqueeEl = document.createElement('div')
            marqueeEl.className = 'marquee'
            marqueeEl.style.left = start.x + 'px'
            marqueeEl.style.top = start.y + 'px'
            marqueeEl.style.width = '1px'
            marqueeEl.style.height = '1px'
            board.appendChild(marqueeEl)
          }, 200)
          const onMove = (ev) => {
            if (!started) return
            const p = getBoardPoint(ev)
            const left = Math.min(start.x, p.x)
            const top = Math.min(start.y, p.y)
            const w = Math.max(1, Math.abs(p.x - start.x))
            const h = Math.max(1, Math.abs(p.y - start.y))
            marqueeEl.style.left = left + 'px'
            marqueeEl.style.top = top + 'px'
            marqueeEl.style.width = w + 'px'
            marqueeEl.style.height = h + 'px'
          }
          const onUp = () => {
            clearTimeout(timer)
            document.removeEventListener('pointermove', onMove)
            document.removeEventListener('pointerup', onUp)
            if (!started) return
            const mRect = marqueeEl.getBoundingClientRect()
            marqueeEl.remove()
            // 优化框选：若框选区域未完全包含图片，但与图片有交叠，则仅选择该图片内部的其他元素
            const allItems = Array.from(items)
            const overlappedImages = allItems.filter((it) => {
              if (!it.querySelector('img')) return false
              const ir = it.getBoundingClientRect()
              return !(
                ir.right < mRect.left ||
                ir.left > mRect.right ||
                ir.bottom < mRect.top ||
                ir.top > mRect.bottom
              )
            })
            const fullyCoveredImages = overlappedImages.filter((it) => {
              const ir = it.getBoundingClientRect()
              return (
                mRect.left <= ir.left &&
                mRect.right >= ir.right &&
                mRect.top <= ir.top &&
                mRect.bottom >= ir.bottom
              )
            })
            let selected = null
            if (overlappedImages.length && fullyCoveredImages.length === 0) {
              // 仅选择图片内部的非图片元素（且与框选区域相交）
              selected = allItems.filter((it) => {
                if (it.querySelector('img')) return false
                const ir = it.getBoundingClientRect()
                const intersect = !(
                  ir.right < mRect.left ||
                  ir.left > mRect.right ||
                  ir.bottom < mRect.top ||
                  ir.top > mRect.bottom
                )
                if (!intersect) return false
                // 必须位于任一交叠图片内部
                return overlappedImages.some((img) => {
                  const r = img.getBoundingClientRect()
                  return (
                    ir.left >= r.left &&
                    ir.right <= r.right &&
                    ir.top >= r.top &&
                    ir.bottom <= r.bottom
                  )
                })
              })
            } else {
              // 默认：选取与框选区域相交的所有对象（含图片）
              selected = allItems.filter((it) => {
                const ir = it.getBoundingClientRect()
                return !(
                  ir.right < mRect.left ||
                  ir.left > mRect.right ||
                  ir.bottom < mRect.top ||
                  ir.top > mRect.bottom
                )
              })
            }
            if (selected.length) {
              setGroupSelection(selected)
              setStatus(`已框选 ${selected.length} 个对象，可整体拖动`)
              // 屏蔽框选结束触发的后续一次空白点击重置
              suppressBoardClickReset = true
              setTimeout(() => (suppressBoardClickReset = false), 0)
            } else {
              clearSelection()
              setStatus('未框选到对象')
            }
          }
          document.addEventListener('pointermove', onMove)
          document.addEventListener('pointerup', onUp)
          return
        }
        // 其他工具允许在图片上操作
        // 文本工具：若点击在已有文字编辑框内，则不创建新文本，直接进入编辑
        if (currentTool === 'text') {
          const existingText = e.target.closest('.text-content')
          if (existingText) {
            // 让浏览器默认行为生效（焦点与光标定位），不阻止默认
            existingText.focus()
            setStatus('正在编辑现有文字')
            return
          }
        }
        // 若点击的是手柄，则不创建新项，交由项自身交互处理
        if (e.target.closest('.handle')) {
          return
        }
        // 若点击在已有项内：
        // - 图片项：允许创建文字覆盖其上
        // - 非图片项（文本/形状/涂鸦等）：不创建新项
        const targetItem = e.target.closest('.item')
        if (targetItem) {
          const isImageItem = !!targetItem.querySelector('img')
          if (!isImageItem) {
            return
          }
        }
        e.preventDefault()
        const p0 = getContentPoint(e)

        // 矩形/椭圆
        if (currentTool === 'rect' || currentTool === 'circle') {
          const item = document.createElement('div')
          item.className = `item shape ${
            currentTool === 'circle' ? 'circle' : 'rect'
          }`
          item.style.zIndex = ++zCounter
          item.style.left = p0.x + 'px'
          item.style.top = p0.y + 'px'
          item.style.width = '1px'
          item.style.height = '1px'
          addHandles(item)
          boardContent.appendChild(item)
          items.add(item)
          selectItem(item)
          updateCount()
          const startX = p0.x
          const startY = p0.y
          const onMove = (ev) => {
            const p = getContentPoint(ev)
            const left = Math.min(startX, p.x)
            const top = Math.min(startY, p.y)
            const w = Math.max(1, Math.abs(p.x - startX))
            const h = Math.max(1, Math.abs(p.y - startY))
            item.style.left = left + 'px'
            item.style.top = top + 'px'
            item.style.width = w + 'px'
            item.style.height = h + 'px'
          }
          const onUp = () => {
            document.removeEventListener('pointermove', onMove)
            document.removeEventListener('pointerup', onUp)
            bindItemInteraction(item)
            pushHistory({ type: 'add', el: item })
          }
          document.addEventListener('pointermove', onMove)
          document.addEventListener('pointerup', onUp)
          return
        }

        // 文本
        if (currentTool === 'text') {
          const item = document.createElement('div')
          item.className = 'item text-item'
          item.style.zIndex = ++zCounter
          item.style.left = p0.x + 'px'
          item.style.top = p0.y + 'px'
          item.style.width = '200px'
          item.style.height = '40px'
          const text = document.createElement('div')
          text.className = 'text-content'
          text.contentEditable = 'false'
          text.textContent = ''
          item.appendChild(text)
          addHandles(item)
          boardContent.appendChild(item)
          items.add(item)
          selectItem(item)
          updateCount()
          bindItemInteraction(item)
          pushHistory({ type: 'add', el: item })
          // 本次点击已用于创建文本，避免随后 click 事件将工具重置为拖动
          suppressBoardClickReset = true
          setTimeout(() => (suppressBoardClickReset = false), 0)
          text.focus()
          text.addEventListener('keydown', (ev) => {
            if (ev.key === 'Escape') text.blur()
          })
          text.addEventListener('blur', () => {
            if (!text.textContent.trim()) {
              items.delete(item)
              item.remove()
              updateCount()
            }
          })
          // 新建文字后，若当前为文字工具，展示配置面板
          updateTextConfigVisibility()
          return
        }

        // 涂鸦（画笔）
        if (currentTool === 'doodle') {
          const item = document.createElement('div')
          item.className = 'item doodle'
          item.style.zIndex = ++zCounter
          const svgNS = 'http://www.w3.org/2000/svg'
          const svg = document.createElementNS(svgNS, 'svg')
          // 保证操作生成的 SVG 无背景色，避免导出差异
          svg.style.background = 'transparent'
          const path = document.createElementNS(svgNS, 'path')
          path.setAttribute(
            'stroke',
            getComputedStyle(document.documentElement)
              .getPropertyValue('--accent-color')
              .trim() || '#667eea'
          )
          path.setAttribute('stroke-width', '3')
          path.setAttribute('fill', 'none')
          svg.appendChild(path)
          item.appendChild(svg)
          boardContent.appendChild(item)
          items.add(item)
          selectItem(item)
          updateCount()
          let points = [{ x: p0.x, y: p0.y }]
          let minX = p0.x,
            minY = p0.y,
            maxX = p0.x,
            maxY = p0.y
          const redraw = () => {
            const d = points
              .map(
                (p, i) => `${i === 0 ? 'M' : 'L'} ${p.x - minX} ${p.y - minY}`
              )
              .join(' ')
            path.setAttribute('d', d)
            const w = Math.max(1, maxX - minX)
            const h = Math.max(1, maxY - minY)
            item.style.left = minX + 'px'
            item.style.top = minY + 'px'
            item.style.width = w + 'px'
            item.style.height = h + 'px'
            svg.setAttribute('viewBox', `0 0 ${w} ${h}`)
          }
          const onMove = (ev) => {
            const p = getContentPoint(ev)
            points.push(p)
            minX = Math.min(minX, p.x)
            minY = Math.min(minY, p.y)
            maxX = Math.max(maxX, p.x)
            maxY = Math.max(maxY, p.y)
            redraw()
          }
          const onUp = () => {
            document.removeEventListener('pointermove', onMove)
            document.removeEventListener('pointerup', onUp)
            addHandles(item)
            bindItemInteraction(item)
            pushHistory({ type: 'add', el: item })
          }
          document.addEventListener('pointermove', onMove)
          document.addEventListener('pointerup', onUp)
          return
        }

        // 箭头
        if (currentTool === 'arrow') {
          const item = document.createElement('div')
          item.className = 'item arrow'
          item.style.zIndex = ++zCounter
          const svgNS = 'http://www.w3.org/2000/svg'
          const svg = document.createElementNS(svgNS, 'svg')
          // 保证操作生成的 SVG 无背景色，避免导出差异
          svg.style.background = 'transparent'
          const main = document.createElementNS(svgNS, 'path')
          const head1 = document.createElementNS(svgNS, 'path')
          const head2 = document.createElementNS(svgNS, 'path')
          ;[main, head1, head2].forEach((p) => {
            p.setAttribute(
              'stroke',
              getComputedStyle(document.documentElement)
                .getPropertyValue('--accent-color')
                .trim() || '#667eea'
            )
            p.setAttribute('stroke-width', '3')
            p.setAttribute('fill', 'none')
            svg.appendChild(p)
          })
          item.appendChild(svg)
          boardContent.appendChild(item)
          items.add(item)
          selectItem(item)
          updateCount()
          const drawArrow = (sx, sy, ex, ey, minX, minY, w, h) => {
            svg.setAttribute('viewBox', `0 0 ${w} ${h}`)
            const lx = sx - minX,
              ly = sy - minY,
              rx = ex - minX,
              ry = ey - minY
            const dx = rx - lx,
              dy = ry - ly
            const len = Math.hypot(dx, dy) || 1
            const ux = dx / len,
              uy = dy / len
            const headLen = 12,
              headWidth = 6
            const hx = rx - ux * headLen
            const hy = ry - uy * headLen
            const perpX = -uy * headWidth
            const perpY = ux * headWidth
            main.setAttribute('d', `M ${lx} ${ly} L ${rx} ${ry}`)
            head1.setAttribute(
              'd',
              `M ${rx} ${ry} L ${hx + perpX} ${hy + perpY}`
            )
            head2.setAttribute(
              'd',
              `M ${rx} ${ry} L ${hx - perpX} ${hy - perpY}`
            )
          }
          const startX = p0.x
          const startY = p0.y
          const onMove = (ev) => {
            const p = getContentPoint(ev)
            // 为箭头增加安全内边距，避免小尺寸下箭头尖与描边被裁剪
            const pad = 12
            const rawMinX = Math.min(startX, p.x)
            const rawMinY = Math.min(startY, p.y)
            const rawW = Math.max(1, Math.abs(p.x - startX))
            const rawH = Math.max(1, Math.abs(p.y - startY))
            const minX = rawMinX - pad
            const minY = rawMinY - pad
            const w = rawW + pad * 2
            const h = rawH + pad * 2
            item.style.left = minX + 'px'
            item.style.top = minY + 'px'
            item.style.width = w + 'px'
            item.style.height = h + 'px'
            drawArrow(startX, startY, p.x, p.y, minX, minY, w, h)
          }
          const onUp = () => {
            document.removeEventListener('pointermove', onMove)
            document.removeEventListener('pointerup', onUp)
            addHandles(item)
            bindItemInteraction(item)
            pushHistory({ type: 'add', el: item })
          }
          document.addEventListener('pointermove', onMove)
          document.addEventListener('pointerup', onUp)
          return
        }
      })

      // 工具项关联图片：优先取交叠面积最大，其次取中心点最近
      function getAssociatedImageItem(el) {
        if (!el) return null
        // 已是图片项则无需关联
        if (el.querySelector('img')) return el
        const elRect = el.getBoundingClientRect()
        const imageItems = Array.from(items).filter(
          (it) => !!it.querySelector('img')
        )
        let bestImg = null
        let bestArea = 0
        for (const imgItem of imageItems) {
          const ir = imgItem.getBoundingClientRect()
          const left = Math.max(elRect.left, ir.left)
          const top = Math.max(elRect.top, ir.top)
          const right = Math.min(elRect.right, ir.right)
          const bottom = Math.min(elRect.bottom, ir.bottom)
          const overlapW = Math.max(0, right - left)
          const overlapH = Math.max(0, bottom - top)
          const area = overlapW * overlapH
          if (area > bestArea) {
            bestArea = area
            bestImg = imgItem
          }
        }
        if (!bestImg && imageItems.length) {
          const scx = (elRect.left + elRect.right) / 2
          const scy = (elRect.top + elRect.bottom) / 2
          let bestDist2 = Infinity
          for (const imgItem of imageItems) {
            const ir = imgItem.getBoundingClientRect()
            const icx = (ir.left + ir.right) / 2
            const icy = (ir.top + ir.bottom) / 2
            const d2 = (icx - scx) * (icx - scx) + (icy - scy) * (icy - scy)
            if (d2 < bestDist2) {
              bestDist2 = d2
              bestImg = imgItem
            }
          }
        }
        return bestImg
      }

      // Alt 悬停：展示选中元素相对鼠标所在元素的Δx/Δy
      board.addEventListener('pointermove', (e) => {
        // 仅在按住 Alt 且存在选中时生效
        const hasSelection =
          !!selectedItem || (selectedGroup && selectedGroup.length > 0)
        if (!e.altKey || !hasSelection) {
          hideDistance()
          return
        }
        // 当前悬停的元素（忽略菜单与画板自身）
        const hoveredItem = e.target.closest('.item')
        if (!hoveredItem) {
          hideDistance()
          return
        }
        // 若悬停在已选中组内的某个元素，则不显示（避免与自身比较）
        if (selectedItem && hoveredItem === selectedItem) {
          hideDistance()
          return
        }
        if (
          selectedGroup &&
          selectedGroup.length &&
          selectedGroup.includes(hoveredItem)
        ) {
          hideDistance()
          return
        }
        // 计算被比较的“选中区域”矩形（单选或组选的总体矩形）
        let selectedRect
        if (selectedGroup && selectedGroup.length) {
          // 多选：仅以所选元素的并集作为选区，不再额外并入关联图片
          const rects = selectedGroup.map((el) => el.getBoundingClientRect())
          const left = Math.min(...rects.map((r) => r.left))
          const top = Math.min(...rects.map((r) => r.top))
          const right = Math.max(...rects.map((r) => r.right))
          const bottom = Math.max(...rects.map((r) => r.bottom))
          selectedRect = {
            left,
            top,
            right,
            bottom,
            width: right - left,
            height: bottom - top,
          }
        } else {
          // 单选：直接以当前选中元素为选区，不再并入关联图片
          selectedRect = selectedItem.getBoundingClientRect()
        }
        let targetRect = hoveredItem.getBoundingClientRect()
        // 单选工具项：当鼠标位于其所属图片内部但非指向选中项时，改为与图片边界计算距离
        if (selectedItem && !selectedItem.querySelector('img')) {
          const assoc = getAssociatedImageItem(selectedItem)
          if (assoc) {
            const ir = assoc.getBoundingClientRect()
            const cx = e.clientX
            const cy = e.clientY
            const insideImage =
              cx >= ir.left && cx <= ir.right && cy >= ir.top && cy <= ir.bottom
            if (insideImage && hoveredItem !== selectedItem) {
              targetRect = ir
            }
          }
        }
        // 计算用于水平/垂直的锚点矩形：
        // - 多选时按“最外层的元素”（靠近目标的那一侧）作为锚点
        // - 单选工具项时按“工具框自身”作为锚点
        let anchors = null
        if (selectedGroup && selectedGroup.length) {
          const groupRects = selectedGroup.map((el) =>
            el.getBoundingClientRect()
          )
          anchors = {}
          // 水平锚点：目标在右侧取 right 最大者；在左侧取 left 最小者
          if (targetRect.left >= selectedRect.right) {
            let best = null
            for (const r of groupRects) {
              if (!best || r.right > best.right) best = r
            }
            anchors.anchorXRect = best
          } else if (targetRect.right <= selectedRect.left) {
            let best = null
            for (const r of groupRects) {
              if (!best || r.left < best.left) best = r
            }
            anchors.anchorXRect = best
          }
          // 垂直锚点：目标在下方取 bottom 最大者；在上方取 top 最小者
          if (targetRect.top >= selectedRect.bottom) {
            let best = null
            for (const r of groupRects) {
              if (!best || r.bottom > best.bottom) best = r
            }
            anchors.anchorYRect = best
          } else if (targetRect.bottom <= selectedRect.top) {
            let best = null
            for (const r of groupRects) {
              if (!best || r.top < best.top) best = r
            }
            anchors.anchorYRect = best
          }
        } else if (selectedItem && !selectedItem.querySelector('img')) {
          // 单选工具项：锚点使用工具框本身
          const r1 = selectedItem.getBoundingClientRect()
          anchors = { anchorXRect: r1, anchorYRect: r1 }
        }
        // 根据鼠标位置计算重叠场景的边偏好（left/right, top/bottom）
        if (!anchors) anchors = {}
        const axRef = anchors.anchorXRect || selectedRect
        const ayRef = anchors.anchorYRect || selectedRect
        const axCenter = (axRef.left + axRef.right) / 2
        const ayCenter = (ayRef.top + ayRef.bottom) / 2
        anchors.prefX = e.clientX <= axCenter ? 'left' : 'right'
        anchors.prefY = e.clientY <= ayCenter ? 'top' : 'bottom'
        showDistanceBetween(selectedRect, targetRect, anchors)
      })

      // 按下 Alt：隐藏对齐线并标记状态
      document.addEventListener('keydown', (e) => {
        if (e.key === 'Alt') {
          altPressed = true
          hideGuides()
        }
      })
      // 松开Alt或离开画板时隐藏距离提示
      document.addEventListener('keyup', (e) => {
        if (e.key === 'Alt') {
          altPressed = false
          hideDistance()
        }
      })
      board.addEventListener('pointerleave', () => hideDistance())

      // 组合为 Canvas（支持：最大图片尺寸 / 内容包裹 / 画板尺寸 / 选中内容）
      function composeCanvas(mode) {
        let itemList = Array.from(items)
        if (!itemList.length) return null
        // 仅选中项导出
        if (mode === 'selected') {
          itemList = itemList.filter((el) => el.classList.contains('selected'))
          if (!itemList.length) return null
        }
        if (mode === 'board') {
          const canvas = document.createElement('canvas')
          const dpr = window.devicePixelRatio || 1
          const targetW = Math.max(
            1,
            Math.round(virtualBoardWidth || board.clientWidth)
          )
          const targetH = Math.max(
            1,
            Math.round(virtualBoardHeight || board.clientHeight)
          )
          canvas.width = Math.round(targetW * dpr)
          canvas.height = Math.round(targetH * dpr)
          const ctx = canvas.getContext('2d')
          // 提升导出清晰度：按设备像素比缩放坐标系
          ctx.scale(dpr, dpr)
          ctx.imageSmoothingEnabled = true
          ctx.imageSmoothingQuality = 'high'
          const cs = getComputedStyle(board)
          if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)') {
            ctx.fillStyle = cs.backgroundColor
            // 经过缩放后，使用目标 CSS 尺寸填充即可
            ctx.fillRect(0, 0, targetW, targetH)
          }
          itemList
            .sort(
              (a, b) =>
                parseInt(a.style.zIndex || '0') -
                parseInt(b.style.zIndex || '0')
            )
            .forEach((el) => drawItemToCanvas(ctx, el, { left: 0, top: 0 }))
          return canvas
        }
        // 计算裁剪范围（largest 或 content）
        let rect
        if (mode === 'largest') {
          const largest = itemList.reduce((best, el) => {
            const area = el.offsetWidth * el.offsetHeight
            return !best || area > best.area ? { el, area } : best
          }, null)
          rect = {
            left: largest.el.offsetLeft,
            top: largest.el.offsetTop,
            width: largest.el.offsetWidth,
            height: largest.el.offsetHeight,
          }
        } else {
          const minLeft = Math.min(...itemList.map((el) => el.offsetLeft))
          const minTop = Math.min(...itemList.map((el) => el.offsetTop))
          const maxRight = Math.max(
            ...itemList.map((el) => el.offsetLeft + el.offsetWidth)
          )
          const maxBottom = Math.max(
            ...itemList.map((el) => el.offsetTop + el.offsetHeight)
          )
          rect = {
            left: minLeft,
            top: minTop,
            width: maxRight - minLeft,
            height: maxBottom - minTop,
          }
        }
        const canvas = document.createElement('canvas')
        const dpr = window.devicePixelRatio || 1
        const targetW = Math.max(1, Math.round(rect.width))
        const targetH = Math.max(1, Math.round(rect.height))
        canvas.width = Math.round(targetW * dpr)
        canvas.height = Math.round(targetH * dpr)
        const ctx = canvas.getContext('2d')
        // 提升导出清晰度：按设备像素比缩放坐标系
        ctx.scale(dpr, dpr)
        ctx.imageSmoothingEnabled = true
        ctx.imageSmoothingQuality = 'high'
        const cs = getComputedStyle(board)
        // 仅在“画板尺寸”模式下填充背景，其余模式保持透明
        if (
          mode === 'board' &&
          cs.backgroundColor &&
          cs.backgroundColor !== 'rgba(0, 0, 0, 0)'
        ) {
          ctx.fillStyle = cs.backgroundColor
          ctx.fillRect(0, 0, targetW, targetH)
        }
        itemList
          .sort(
            (a, b) =>
              parseInt(a.style.zIndex || '0') - parseInt(b.style.zIndex || '0')
          )
          .forEach((el) => drawItemToCanvas(ctx, el, rect))
        return canvas
      }

      // 文本换行辅助：按最大宽度将字符串拆分为多行
      function wrapTextToLines(ctx, text, maxWidth) {
        const lines = []
        let current = ''
        for (const ch of text) {
          const test = current + ch
          if (ctx.measureText(test).width <= maxWidth) {
            current = test
          } else {
            if (current) lines.push(current)
            current = ch
          }
        }
        if (current) lines.push(current)
        return lines
      }

      function parseLineHeight(cs, fontSize) {
        const lh = cs.lineHeight
        if (!lh || lh === 'normal') return Math.round(fontSize * 1.4)
        const n = parseFloat(lh)
        if (!isNaN(n)) {
          // 若为纯数字，可能是倍数；若带 px，parseFloat 也能得到数值像素
          if (String(lh).includes('px')) return Math.round(n)
          return Math.round(n * fontSize)
        }
        return Math.round(fontSize * 1.4)
      }

      function drawItemToCanvas(ctx, el, rect) {
        const dx = el.offsetLeft - (rect.left || 0)
        const dy = el.offsetTop - (rect.top || 0)
        const w = el.offsetWidth
        const h = el.offsetHeight
        const accent =
          getComputedStyle(document.documentElement)
            .getPropertyValue('--accent-color')
            .trim() || '#667eea'
        // 图片
        const img = el.querySelector('img')
        if (img && img.complete) {
          try {
            ctx.drawImage(img, dx, dy, w, h)
          } catch (e) {
            console.warn('绘制图片失败', e)
          }
          return
        }
        // 形状
        if (el.classList.contains('shape')) {
          ctx.strokeStyle = accent
          ctx.lineWidth = 3
          // 页面为虚线样式，这里匹配并使用圆角端点
          ctx.setLineDash([8, 6])
          ctx.lineCap = 'round'
          ctx.lineJoin = 'round'
          if (el.classList.contains('rect')) {
            ctx.strokeRect(dx, dy, w, h)
          } else if (el.classList.contains('circle')) {
            ctx.beginPath()
            ctx.ellipse(
              dx + w / 2,
              dy + h / 2,
              Math.max(1, w / 2),
              Math.max(1, h / 2),
              0,
              0,
              Math.PI * 2
            )
            ctx.stroke()
          }
          // 重置线段样式，以免影响后续绘制
          ctx.setLineDash([])
          return
        }
        // 文本
        if (el.classList.contains('text-item')) {
          const textEl = el.querySelector('.text-content')
          const text = (textEl?.textContent || '').trim()
          if (!text) return
          const cs = getComputedStyle(textEl)
          const fontSize = parseInt(cs.fontSize, 10) || 16
          const fontFamily = cs.fontFamily || 'system-ui'
          const fontWeight = cs.fontWeight || 'normal'
          const fontStyle = cs.fontStyle || 'normal'
          const lineHeight = parseLineHeight(cs, fontSize)
          ctx.fillStyle = cs.color || '#1a202c'
          ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`
          ctx.textBaseline = 'top'
          const padL = 8
          const padT = 6
          const availW = Math.max(1, w - padL * 2)
          const paragraphs = text.replace(/\r/g, '').split(/\n/)
          let y = dy + padT
          const x = dx + padL
          for (const p of paragraphs) {
            const lines = wrapTextToLines(ctx, p, availW)
            for (const ln of lines) {
              ctx.fillText(ln, x, y)
              y += lineHeight
            }
          }
          return
        }
        // 涂鸦
        if (el.classList.contains('doodle')) {
          const path = el.querySelector('path')
          const d = path?.getAttribute('d') || ''
          ctx.strokeStyle = accent
          ctx.lineWidth = 3
          ctx.beginPath()
          const pts = d
            .trim()
            .split(/[ML]\s*/)
            .map((s) => s.trim())
            .filter(Boolean)
            .map((pt) => pt.split(/\s+/).map(Number))
          pts.forEach((xy, i) => {
            const px = dx + (xy[0] || 0)
            const py = dy + (xy[1] || 0)
            if (i === 0) ctx.moveTo(px, py)
            else ctx.lineTo(px, py)
          })
          ctx.stroke()
          return
        }
        // 箭头
        if (el.classList.contains('arrow')) {
          const paths = el.querySelectorAll('path')
          ctx.strokeStyle = accent
          ctx.lineWidth = 3
          paths.forEach((p) => {
            const d = p.getAttribute('d') || ''
            ctx.beginPath()
            const pts = d
              .trim()
              .split(/[ML]\s*/)
              .map((s) => s.trim())
              .filter(Boolean)
              .map((pt) => pt.split(/\s+/).map(Number))
            pts.forEach((xy, i) => {
              const px = dx + (xy[0] || 0)
              const py = dy + (xy[1] || 0)
              if (i === 0) ctx.moveTo(px, py)
              else ctx.lineTo(px, py)
            })
            ctx.stroke()
          })
          return
        }
      }

      // 导出画板为 PNG（统一为：所有内容区域）
      btnExport.addEventListener('click', async () => {
        const itemList = Array.from(items)
        if (!itemList.length) {
          setStatus('画板为空，无法导出')
          return
        }
        const mode = 'content' // 固定为所有内容区域
        try {
          btnExport.disabled = true
          setStatus('正在导出 PNG…（所有内容区域）')
          const canvas = composeCanvas(mode)
          const dataUrl = canvas.toDataURL('image/png')
          triggerDownload(dataUrl)
          setStatus('已导出 PNG（所有内容区域）')
        } catch (err) {
          console.error(err)
          setStatus('导出失败，请重试')
        } finally {
          btnExport.disabled = false
        }
      })

      // 导出所选为 PNG（仅导出选中对象的包裹区域）
      btnExportSelected.addEventListener('click', async () => {
        const selected = Array.from(items).filter((el) =>
          el.classList.contains('selected')
        )
        if (!selected.length) {
          setStatus('请先选中要导出的对象')
          return
        }
        try {
          btnExportSelected.disabled = true
          setStatus(`正在导出 PNG…（所选 ${selected.length} 个对象）`)
          const canvas = composeCanvas('selected')
          if (!canvas) {
            setStatus('请先选中要导出的对象')
            return
          }
          const dataUrl = canvas.toDataURL('image/png')
          triggerDownload(dataUrl)
          setStatus('已导出 PNG（所选对象）')
        } catch (err) {
          console.error(err)
          setStatus('导出失败，请重试')
        } finally {
          btnExportSelected.disabled = false
        }
      })

      function triggerDownload(dataUrl) {
        const a = document.createElement('a')
        a.href = dataUrl
        a.download = 'canvas-board.png'
        document.body.appendChild(a)
        a.click()
        a.remove()
      }

      // Canvas 转 Blob（Promise 形式）
      function canvasToBlobAsync(canvas, type = 'image/png', quality = 1.0) {
        return new Promise((resolve, reject) => {
          try {
            canvas.toBlob((blob) => resolve(blob), type, quality)
          } catch (e) {
            reject(e)
          }
        })
      }

      // 写入剪贴板（image/png）
      async function writeImageBlobToClipboard(blob) {
        if (!blob) throw new Error('No blob')
        if (!navigator.clipboard || typeof ClipboardItem === 'undefined') {
          throw new Error('Clipboard API not supported')
        }
        const item = new ClipboardItem({ [blob.type || 'image/png']: blob })
        await navigator.clipboard.write([item])
      }

      // 识别二维码（从组合 Canvas 中提取像素）
      function scanBoard() {
        const itemList = Array.from(items)
        if (!itemList.length) {
          setStatus('画板为空，无法识别二维码')
          return
        }
        const canvas = composeCanvas('content')
        const ctx = canvas.getContext('2d')
        const img = ctx.getImageData(0, 0, canvas.width, canvas.height)
        try {
          const result = jsQR(img.data, canvas.width, canvas.height)
          if (result) {
            setStatus(`二维码：${result.data}`)
            alert(`识别到二维码内容:\n${result.data}`)
          } else {
            setStatus('未识别到二维码')
          }
        } catch (e) {
          console.error(e)
          setStatus('二维码识别失败')
        }
      }

      // 清空画板
      btnClear.addEventListener('click', clearBoard)

      // 空格长按拖动画板：按下启动、松开退出
      function isTextInputTarget(target) {
        if (!target) return false
        return (
          target.isContentEditable ||
          target.tagName === 'INPUT' ||
          target.tagName === 'TEXTAREA'
        )
      }

      document.addEventListener('keydown', (e) => {
        if (e.code === 'Space' || e.key === ' ') {
          if (isTextInputTarget(e.target)) return
          // 已进入模式或正在计时则仅阻止默认
          if (isPanMode || panKeyTimer) {
            e.preventDefault()
            return
          }
          // 长按 200ms 进入拖拽模式
          panKeyTimer = setTimeout(() => {
            isPanMode = true
            board.classList.add('pan-mode')
            const selectedCount = Array.from(items).filter((it) =>
              it.classList.contains('selected')
            ).length
            if (selectedCount > 1) {
              setStatus('按住空格拖动选中对象（松开退出）')
            } else {
              setStatus('按住空格拖动画板（松开退出）')
            }
            panKeyTimer = null
          }, 200)
          e.preventDefault()
        }
      })

      document.addEventListener('keyup', (e) => {
        if (e.code === 'Space' || e.key === ' ') {
          if (panKeyTimer) {
            clearTimeout(panKeyTimer)
            panKeyTimer = null
          }
          if (isPanning) {
            isPanning = false
            board.classList.remove('pan-active')
          }
          if (isPanMode) {
            isPanMode = false
            board.classList.remove('pan-mode')
            setStatus('已退出拖动画板')
          }
        }
      })

      // 画板尺寸输入事件：更新虚拟画板尺寸（用于“画板尺寸”导出）
      function parsePositiveInt(val, fallback) {
        const n = parseInt(val, 10)
        return isNaN(n) || n <= 0 ? fallback : n
      }
      boardWidthInput?.addEventListener('change', () => {
        const w = parsePositiveInt(boardWidthInput.value, virtualBoardWidth)
        virtualBoardWidth = w
        boardWidthInput.value = String(w)
        setStatus(`画板宽度设为 ${w} 像素`)
      })
      boardHeightInput?.addEventListener('change', () => {
        const h = parsePositiveInt(boardHeightInput.value, virtualBoardHeight)
        virtualBoardHeight = h
        boardHeightInput.value = String(h)
        setStatus(`画板高度设为 ${h} 像素`)
      })

      // 可通过 ESC 取消选中
      document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') {
          items.forEach((it) => it.classList.remove('selected'))
          selectedItem = null
          hideContextMenu()
        }
        // Delete 删除选中图片
        if (e.key === 'Delete') {
          const selected = Array.from(items).filter((it) =>
            it.classList.contains('selected')
          )
          if (selected.length > 0) {
            selected.forEach((it) => {
              items.delete(it)
              it.remove()
            })
            updateCount()
            setStatus(`已删除 ${selected.length} 张图片`)
            selectedItem = null
            hideContextMenu()
          }
        }
      })

      // 点击空白处隐藏菜单（仅左键）
      document.addEventListener('mousedown', (e) => {
        if (e.button !== 0) return
        if (!ctxMenu.contains(e.target)) hideContextMenu()
      })
      // 在菜单上禁用默认右键菜单，避免被系统菜单覆盖
      ctxMenu.addEventListener('contextmenu', (e) => {
        e.preventDefault()
        e.stopPropagation()
      })
      ctxMenu.addEventListener('click', (e) => {
        e.stopPropagation()
      })
      // 菜单删除
      ctxDelete.addEventListener('click', () => {
        if (selectedItem && items.has(selectedItem)) {
          const el = selectedItem
          el.remove()
          items.delete(el)
          pushHistory({ type: 'remove', el })
          setStatus('已通过菜单删除选中对象')
          selectedItem = null
          updateCount()
          hideContextMenu()
        }
      })

      // 菜单工具选择
      ctxToolMove.addEventListener('click', () => {
        setTool('move')
        hideContextMenu()
      })
      // 旧按钮（涂鸦/画圈）已替换为画笔/椭圆
      ctxToolBrush.addEventListener('click', () => {
        setTool('brush')
        hideContextMenu()
      })
      ctxToolArrow.addEventListener('click', () => {
        setTool('arrow')
        hideContextMenu()
      })
      ctxToolEllipse.addEventListener('click', () => {
        setTool('ellipse')
        hideContextMenu()
      })
      ctxToolRect.addEventListener('click', () => {
        setTool('rect')
        hideContextMenu()
      })
      ctxToolText.addEventListener('click', () => {
        setTool('text')
        hideContextMenu()
      })

      // 菜单动作
      ctxUndo.addEventListener('click', () => undoAction())
      ctxSave.addEventListener('click', () => btnExport.click())
      ctxCopyPNG.addEventListener('click', async () => {
        const itemList = Array.from(items)
        if (!itemList.length) {
          setStatus('画板为空，无法写入剪贴板')
          return
        }
        try {
          ctxCopyPNG.disabled = true
          setStatus('正在写入剪贴板…（所有内容区域）')
          const canvas = composeCanvas('content')
          const blob = await canvasToBlobAsync(canvas, 'image/png')
          if (!blob) throw new Error('生成 PNG 失败')
          await writeImageBlobToClipboard(blob)
          setStatus('已写入剪贴板（所有内容区域）')
        } catch (e) {
          console.error(e)
          setStatus('写入剪贴板失败或不支持')
          alert('写入剪贴板失败或不支持当前浏览器')
        } finally {
          ctxCopyPNG.disabled = false
          hideContextMenu()
        }
      })
      ctxScan.addEventListener('click', () => scanBoard())

      // 全局颜色应用：工具栏颜色选择器
      const globalColorInput = document.getElementById('globalColor')
      // 对齐辅助线元素与吸附阈值
      let guideXEl = null
      let guideYEl = null
      const snapThreshold = 8
      // Alt 键状态（按住时仅展示距离线，不展示对齐线）
      let altPressed = false

      function ensureGuideEls() {
        if (!guideXEl) {
          guideXEl = document.createElement('div')
          guideXEl.className = 'align-guide x'
          board.appendChild(guideXEl)
        }
        if (!guideYEl) {
          guideYEl = document.createElement('div')
          guideYEl.className = 'align-guide y'
          board.appendChild(guideYEl)
        }
      }
      function showGuideX(x) {
        if (altPressed) {
          if (guideXEl) guideXEl.style.display = 'none'
          return
        }
        ensureGuideEls()
        guideXEl.style.display = 'block'
        guideXEl.style.left = x * boardScale + boardTranslateX + 'px'
      }
      function showGuideY(y) {
        if (altPressed) {
          if (guideYEl) guideYEl.style.display = 'none'
          return
        }
        ensureGuideEls()
        guideYEl.style.display = 'block'
        guideYEl.style.top = y * boardScale + boardTranslateY + 'px'
      }
      function hideGuides() {
        if (guideXEl) guideXEl.style.display = 'none'
        if (guideYEl) guideYEl.style.display = 'none'
      }

      // 距离提示（Alt 悬停显示）：改为对齐线段样式
      let measureXEl = null
      let measureYEl = null
      let measureXLabel = null
      let measureYLabel = null
      function ensureDistanceEl() {
        if (!measureXEl) {
          measureXEl = document.createElement('div')
          measureXEl.className = 'measure-line x'
          measureXLabel = document.createElement('span')
          measureXLabel.className = 'label'
          measureXEl.appendChild(measureXLabel)
          board.appendChild(measureXEl)
        }
        if (!measureYEl) {
          measureYEl = document.createElement('div')
          measureYEl.className = 'measure-line y'
          measureYLabel = document.createElement('span')
          measureYLabel.className = 'label'
          measureYEl.appendChild(measureYLabel)
          board.appendChild(measureYEl)
        }
      }
      function hideDistance() {
        if (measureXEl) measureXEl.style.display = 'none'
        if (measureYEl) measureYEl.style.display = 'none'
      }
      function showDistanceBetween(selectedRect, targetRect, anchorEl) {
        ensureDistanceEl()
        const boardRect = board.getBoundingClientRect()
        // 转为画板内坐标，避免视口偏移导致的定位误差
        const sr = {
          left: Math.round(selectedRect.left - boardRect.left),
          top: Math.round(selectedRect.top - boardRect.top),
          right: Math.round(selectedRect.right - boardRect.left),
          bottom: Math.round(selectedRect.bottom - boardRect.top),
          width: Math.round(
            selectedRect.width || selectedRect.right - selectedRect.left
          ),
          height: Math.round(
            selectedRect.height || selectedRect.bottom - selectedRect.top
          ),
        }
        const tr = {
          left: Math.round(targetRect.left - boardRect.left),
          top: Math.round(targetRect.top - boardRect.top),
          right: Math.round(targetRect.right - boardRect.left),
          bottom: Math.round(targetRect.bottom - boardRect.top),
          width: Math.round(targetRect.width),
          height: Math.round(targetRect.height),
        }
        // 可选的锚点矩形（分别用于水平/垂直），以支持“单选工具项按自身框计算”及“多选时按最外层元素计算”
        let ax = null
        let ay = null
        let prefX = null
        let prefY = null
        if (anchorEl && typeof anchorEl === 'object') {
          if (anchorEl.anchorXRect) {
            const a = anchorEl.anchorXRect
            ax = {
              left: Math.round(a.left - boardRect.left),
              top: Math.round(a.top - boardRect.top),
              right: Math.round(a.right - boardRect.left),
              bottom: Math.round(a.bottom - boardRect.top),
              width: Math.round(a.width || a.right - a.left),
              height: Math.round(a.height || a.bottom - a.top),
            }
          }
          if (anchorEl.anchorYRect) {
            const a = anchorEl.anchorYRect
            ay = {
              left: Math.round(a.left - boardRect.left),
              top: Math.round(a.top - boardRect.top),
              right: Math.round(a.right - boardRect.left),
              bottom: Math.round(a.bottom - boardRect.top),
              width: Math.round(a.width || a.right - a.left),
              height: Math.round(a.height || a.bottom - a.top),
            }
          }
          if (anchorEl.prefX) prefX = anchorEl.prefX
          if (anchorEl.prefY) prefY = anchorEl.prefY
        }
        // 水平方向：以选中元素的右/左边与目标的左/右边作为基准，绘制水平线段
        {
          const anchorCY = Math.round(
            (ax ? ax.top : sr.top) + (ax ? ax.height : sr.height) / 2
          )
          let x1, x2
          if (sr.right <= tr.left) {
            // 选中在左，距离为 target.left - selected.right
            x1 = ax ? ax.right : sr.right
            x2 = tr.left
          } else if (sr.left >= tr.right) {
            // 选中在右，距离为 selected.left - target.right
            x1 = tr.right
            x2 = ax ? ax.left : sr.left
          }
          if (x1 != null && x2 != null) {
            const width = Math.max(0, x2 - x1)
            measureXEl.style.left = (x1 || 0) + 'px'
            measureXEl.style.top = anchorCY + 'px'
            measureXEl.style.width = width + 'px'
            measureXEl.style.display = width > 0 ? 'block' : 'none'
            const dxAbs = width
            const dxContent = Math.round(dxAbs / boardScale)
            measureXLabel.textContent = `x轴: ${dxContent}px`
            // 标签以选中元素一侧居中展示
            measureXLabel.style.top = '0px'
            if (sr.right <= tr.left) {
              // 贴近选中元素的右边（线段左端）
              measureXLabel.style.left = '0px'
              measureXLabel.style.transform = 'translate(-100%, -50%)'
            } else if (sr.left >= tr.right) {
              // 贴近选中元素的左边（线段右端）
              measureXLabel.style.left = width + 'px'
              measureXLabel.style.transform = 'translate(0, -50%)'
            }
          } else {
            // 重叠时按鼠标偏好选择边：left/left 或 right/right 差值
            const sx =
              prefX === 'left'
                ? ax
                  ? ax.left
                  : sr.left
                : ax
                ? ax.right
                : sr.right
            const tx = prefX === 'left' ? tr.left : tr.right
            const xMin = Math.min(sx, tx)
            const width = Math.abs(tx - sx)
            measureXEl.style.left = xMin + 'px'
            measureXEl.style.top = anchorCY + 'px'
            measureXEl.style.width = width + 'px'
            measureXEl.style.display = width > 0 ? 'block' : 'none'
            const dxAbs = width
            measureXLabel.textContent = `x轴: ${dxAbs}px`
            // 标签贴近选中元素的侧边
            measureXLabel.style.top = '0px'
            measureXLabel.style.left = sx - xMin + 'px'
            if (prefX === 'left') {
              measureXLabel.style.transform = 'translate(-100%, -50%)'
            } else {
              measureXLabel.style.transform = 'translate(0, -50%)'
            }
          }
        }
        // 垂直方向：以选中元素的下/上边与目标的上/下边作为基准，绘制竖直线段
        {
          const anchorCX = Math.round(
            (ay ? ay.left : sr.left) + (ay ? ay.width : sr.width) / 2
          )
          let y1, y2
          if (sr.bottom <= tr.top) {
            // 选中在上，距离为 target.top - selected.bottom
            y1 = ay ? ay.bottom : sr.bottom
            y2 = tr.top
          } else if (sr.top >= tr.bottom) {
            // 选中在下，距离为 selected.top - target.bottom
            y1 = tr.bottom
            y2 = ay ? ay.top : sr.top
          }
          if (y1 != null && y2 != null) {
            const height = Math.max(0, y2 - y1)
            measureYEl.style.left = anchorCX + 'px'
            measureYEl.style.top = (y1 || 0) + 'px'
            measureYEl.style.height = height + 'px'
            measureYEl.style.display = height > 0 ? 'block' : 'none'
            const dyAbs = height
            const dyContent = Math.round(dyAbs / boardScale)
            measureYLabel.textContent = `y轴: ${dyContent}px`
            // 标签以选中元素顶部/底部居中展示
            measureYLabel.style.left = '0px'
            if (sr.bottom <= tr.top) {
              // 顶部端（靠近选中元素的下边）
              measureYLabel.style.top = '0px'
              measureYLabel.style.transform = 'translate(-50%, -100%)'
            } else if (sr.top >= tr.bottom) {
              // 底部端（靠近选中元素的上边）
              measureYLabel.style.top = height + 'px'
              measureYLabel.style.transform = 'translate(-50%, 0)'
            }
          } else {
            // 重叠时按鼠标偏好选择边：top/top 或 bottom/bottom 差值
            const sy =
              prefY === 'top'
                ? ay
                  ? ay.top
                  : sr.top
                : ay
                ? ay.bottom
                : sr.bottom
            const ty = prefY === 'top' ? tr.top : tr.bottom
            const yMin = Math.min(sy, ty)
            const height = Math.abs(ty - sy)
            measureYEl.style.left = anchorCX + 'px'
            measureYEl.style.top = yMin + 'px'
            measureYEl.style.height = height + 'px'
            measureYEl.style.display = height > 0 ? 'block' : 'none'
            const dyAbs = height
            const dyContent = Math.round(dyAbs / boardScale)
            measureYLabel.textContent = `y轴: ${dyContent}px`
            // 标签贴近选中元素的侧边
            measureYLabel.style.left = '0px'
            measureYLabel.style.top = sy - yMin + 'px'
            if (prefY === 'top') {
              measureYLabel.style.transform = 'translate(-50%, -100%)'
            } else {
              measureYLabel.style.transform = 'translate(-50%, 0)'
            }
          }
        }
      }
      function applyGlobalColor(hex) {
        // 更新主色与梯度
        document.documentElement.style.setProperty('--accent-color', hex)
        document.documentElement.style.setProperty(
          '--accent-gradient',
          `linear-gradient(135deg, ${hex} 0%, ${hex} 100%)`
        )
        // 操作栏与文本项使用同一主色
        document.documentElement.style.setProperty('--text-primary', hex)
        // 计算 RGB，供半透明样式使用
        const toRgb = (c) => {
          const s = c.trim()
          if (s.startsWith('#')) {
            const v = s.slice(1)
            const r =
              v.length === 3
                ? parseInt(v[0] + v[0], 16)
                : parseInt(v.slice(0, 2), 16)
            const g =
              v.length === 3
                ? parseInt(v[1] + v[1], 16)
                : parseInt(v.slice(2, 4), 16)
            const b =
              v.length === 3
                ? parseInt(v[2] + v[2], 16)
                : parseInt(v.slice(4, 6), 16)
            return `${r}, ${g}, ${b}`
          }
          const m = s.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i)
          if (m) return `${m[1]}, ${m[2]}, ${m[3]}`
          return '102, 126, 234'
        }
        document.documentElement.style.setProperty('--accent-rgb', toRgb(hex))
        setStatus('已应用全局颜色')
      }
      if (globalColorInput) {
        globalColorInput.addEventListener('input', (e) => {
          applyGlobalColor(e.target.value)
        })
      }
    </script>
  </body>
</html>
